This commit adds a complete email authentication flow for the Telegram bot, allowing users to login with email + password instead of web app linking codes. Includes critical bug fixes for Oracle integration. **New Features:** - Email-based 2FA authentication with 6-digit codes sent via SMTP - Backend endpoints: verify-email and login-with-email - ConversationHandler for email authentication flow in Telegram bot - Session token verification to prevent user ID spoofing - Rate limiting (5 attempts per 5 minutes) - Email code expiry (5 minutes) with automatic cleanup **Bug Fixes:** - Fixed Oracle column name: ACTIV → INACTIV (with inverted logic) - Fixed Oracle password verification: verificautilizator returns checksum, not user_id - Fixed username case sensitivity: Oracle usernames must be uppercase - Fixed SMTP connection: use start_tls parameter instead of manual STARTTLS - Added middleware exclusions for public email auth endpoints **Backend Changes:** - Added verify-email endpoint (public) in telegram.py - Added login-with-email endpoint (public) with rate limiting and session verification - Updated middleware exclusions in main.py and auth_middleware_wrapper.py - Added AUTH_SESSION_SECRET configuration for session token signing **Telegram Bot Changes:** - New modules: app/auth/email_auth.py, app/bot/email_handlers.py - New utilities: app/utils/email_service.py (SMTP email sending) - Updated handlers.py: ignore callbacks handled by ConversationHandler - Updated menus.py: show Login button for unauthenticated users - Updated API client: verify_email() and login_with_email() methods - Database: email_auth_codes table with cleanup task **Configuration:** - Added SMTP configuration to telegram-bot .env.example - Added AUTH_SESSION_SECRET to backend .env.example - Updated .gitignore: exclude temporary files (*.pid, *.checksum, test scripts) **Dependencies:** - Added aiosmtplib for async SMTP email sending 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
317 lines
11 KiB
Python
317 lines
11 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,
|
|
cleanup_expired_email_codes
|
|
)
|
|
|
|
# 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 email authentication handler
|
|
from app.bot.email_handlers import email_login_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 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)
|