Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot
Modern ERP Reports Application with microservices architecture Tech Stack: - Backend: FastAPI + python-oracledb (Oracle DB integration) - Frontend: Vue.js 3 + PrimeVue + Vite - Telegram Bot: python-telegram-bot + SQLite - Infrastructure: Shared database pool, JWT authentication, SSH tunnel Features: - FastAPI backend with async Oracle connection pool - Vue.js 3 responsive frontend with PrimeVue components - Telegram bot alternative interface - Microservices architecture with shared components - Complete deployment support (Linux Docker + Windows IIS) - Comprehensive testing (Playwright E2E + pytest) Repository Structure: - reports-app/ - Main application (backend, frontend, telegram-bot) - shared/ - Shared components (database pool, auth, utils) - deployment/ - Deployment scripts (Linux & Windows) - docs/ - Project documentation - security/ - Security scanning and git hooks
This commit is contained in:
68
reports-app/telegram-bot/app/db/__init__.py
Normal file
68
reports-app/telegram-bot/app/db/__init__.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Database module for Telegram Bot
|
||||
|
||||
Provides SQLite database operations for:
|
||||
- User management and Oracle account linking
|
||||
- Authentication code management
|
||||
- Conversation session management
|
||||
"""
|
||||
|
||||
from .database import (
|
||||
init_database,
|
||||
get_db_connection,
|
||||
cleanup_expired_codes,
|
||||
cleanup_expired_sessions,
|
||||
get_database_stats,
|
||||
DB_PATH,
|
||||
)
|
||||
|
||||
from .operations import (
|
||||
# User operations
|
||||
create_or_update_user,
|
||||
get_user,
|
||||
link_user_to_oracle,
|
||||
update_user_tokens,
|
||||
update_user_last_active,
|
||||
is_user_linked,
|
||||
# Auth code operations
|
||||
create_auth_code,
|
||||
get_auth_code,
|
||||
verify_and_use_auth_code,
|
||||
get_pending_codes_for_user,
|
||||
# Session operations
|
||||
create_session,
|
||||
get_session,
|
||||
get_user_active_session,
|
||||
update_session_state,
|
||||
delete_session,
|
||||
delete_user_sessions,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Database setup
|
||||
'init_database',
|
||||
'get_db_connection',
|
||||
'cleanup_expired_codes',
|
||||
'cleanup_expired_sessions',
|
||||
'get_database_stats',
|
||||
'DB_PATH',
|
||||
# User operations
|
||||
'create_or_update_user',
|
||||
'get_user',
|
||||
'link_user_to_oracle',
|
||||
'update_user_tokens',
|
||||
'update_user_last_active',
|
||||
'is_user_linked',
|
||||
# Auth code operations
|
||||
'create_auth_code',
|
||||
'get_auth_code',
|
||||
'verify_and_use_auth_code',
|
||||
'get_pending_codes_for_user',
|
||||
# Session operations
|
||||
'create_session',
|
||||
'get_session',
|
||||
'get_user_active_session',
|
||||
'update_session_state',
|
||||
'delete_session',
|
||||
'delete_user_sessions',
|
||||
]
|
||||
243
reports-app/telegram-bot/app/db/database.py
Normal file
243
reports-app/telegram-bot/app/db/database.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
SQLite Database Setup for Telegram Bot
|
||||
|
||||
This module handles database connection, initialization, and schema creation.
|
||||
Uses aiosqlite for async SQLite operations.
|
||||
"""
|
||||
|
||||
import aiosqlite
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Database file location
|
||||
DB_DIR = Path(__file__).parent.parent.parent / "data"
|
||||
DB_PATH = DB_DIR / "telegram_bot.db"
|
||||
|
||||
|
||||
async def get_db_connection() -> aiosqlite.Connection:
|
||||
"""
|
||||
Get a database connection.
|
||||
|
||||
Returns:
|
||||
aiosqlite.Connection: Database connection
|
||||
"""
|
||||
conn = await aiosqlite.connect(DB_PATH)
|
||||
conn.row_factory = aiosqlite.Row # Enable column access by name
|
||||
return conn
|
||||
|
||||
|
||||
async def init_database() -> None:
|
||||
"""
|
||||
Initialize the database and create all tables.
|
||||
Safe to call multiple times - only creates tables if they don't exist.
|
||||
"""
|
||||
try:
|
||||
# Ensure data directory exists
|
||||
DB_DIR.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"Database directory: {DB_DIR}")
|
||||
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
# Enable foreign keys
|
||||
await db.execute("PRAGMA foreign_keys = ON")
|
||||
|
||||
# Create telegram_users table
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS telegram_users (
|
||||
telegram_user_id INTEGER PRIMARY KEY,
|
||||
username TEXT,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT,
|
||||
oracle_username TEXT,
|
||||
jwt_token TEXT,
|
||||
jwt_refresh_token TEXT,
|
||||
token_expires_at TIMESTAMP,
|
||||
linked_at TIMESTAMP,
|
||||
last_active_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT 1
|
||||
)
|
||||
""")
|
||||
|
||||
# Create telegram_auth_codes table
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS telegram_auth_codes (
|
||||
code TEXT PRIMARY KEY,
|
||||
telegram_user_id INTEGER,
|
||||
oracle_username TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
used BOOLEAN DEFAULT 0,
|
||||
used_at TIMESTAMP,
|
||||
FOREIGN KEY (telegram_user_id) REFERENCES telegram_users(telegram_user_id)
|
||||
)
|
||||
""")
|
||||
|
||||
# Create telegram_sessions table
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS telegram_sessions (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
telegram_user_id INTEGER NOT NULL,
|
||||
conversation_state TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
FOREIGN KEY (telegram_user_id) REFERENCES telegram_users(telegram_user_id)
|
||||
)
|
||||
""")
|
||||
|
||||
# Create indexes for better query performance
|
||||
await db.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_auth_codes_telegram_user
|
||||
ON telegram_auth_codes(telegram_user_id)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_auth_codes_expires
|
||||
ON telegram_auth_codes(expires_at)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_telegram_user
|
||||
ON telegram_sessions(telegram_user_id)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_expires
|
||||
ON telegram_sessions(expires_at)
|
||||
""")
|
||||
|
||||
await db.commit()
|
||||
logger.info("Database initialized successfully")
|
||||
|
||||
# Log table info
|
||||
cursor = await db.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table'
|
||||
ORDER BY name
|
||||
""")
|
||||
tables = await cursor.fetchall()
|
||||
logger.info(f"Existing tables: {[t[0] for t in tables]}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize database: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def cleanup_expired_codes() -> int:
|
||||
"""
|
||||
Delete expired authentication codes from the database.
|
||||
This should be called periodically (e.g., every hour).
|
||||
|
||||
Returns:
|
||||
int: Number of expired codes deleted
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
DELETE FROM telegram_auth_codes
|
||||
WHERE expires_at < ?
|
||||
""", (datetime.now(),))
|
||||
|
||||
await db.commit()
|
||||
deleted = cursor.rowcount
|
||||
|
||||
if deleted > 0:
|
||||
logger.info(f"Cleaned up {deleted} expired auth codes")
|
||||
|
||||
return deleted
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cleanup expired codes: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
async def cleanup_expired_sessions() -> int:
|
||||
"""
|
||||
Delete expired sessions from the database.
|
||||
This should be called periodically (e.g., daily).
|
||||
|
||||
Returns:
|
||||
int: Number of expired sessions deleted
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
DELETE FROM telegram_sessions
|
||||
WHERE expires_at < ?
|
||||
""", (datetime.now(),))
|
||||
|
||||
await db.commit()
|
||||
deleted = cursor.rowcount
|
||||
|
||||
if deleted > 0:
|
||||
logger.info(f"Cleaned up {deleted} expired sessions")
|
||||
|
||||
return deleted
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cleanup expired sessions: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
async def get_database_stats() -> dict:
|
||||
"""
|
||||
Get database statistics for monitoring.
|
||||
|
||||
Returns:
|
||||
dict: Database statistics
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
stats = {}
|
||||
|
||||
# Count users
|
||||
cursor = await db.execute("SELECT COUNT(*) FROM telegram_users")
|
||||
stats['total_users'] = (await cursor.fetchone())[0]
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT COUNT(*) FROM telegram_users WHERE is_active = 1"
|
||||
)
|
||||
stats['active_users'] = (await cursor.fetchone())[0]
|
||||
|
||||
# Count pending codes
|
||||
cursor = await db.execute("""
|
||||
SELECT COUNT(*) FROM telegram_auth_codes
|
||||
WHERE used = 0 AND expires_at > ?
|
||||
""", (datetime.now(),))
|
||||
stats['pending_codes'] = (await cursor.fetchone())[0]
|
||||
|
||||
# Count active sessions
|
||||
cursor = await db.execute("""
|
||||
SELECT COUNT(*) FROM telegram_sessions
|
||||
WHERE expires_at > ?
|
||||
""", (datetime.now(),))
|
||||
stats['active_sessions'] = (await cursor.fetchone())[0]
|
||||
|
||||
# Database file size
|
||||
if DB_PATH.exists():
|
||||
stats['db_size_mb'] = DB_PATH.stat().st_size / (1024 * 1024)
|
||||
else:
|
||||
stats['db_size_mb'] = 0
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get database stats: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
# Export main functions
|
||||
__all__ = [
|
||||
'get_db_connection',
|
||||
'init_database',
|
||||
'cleanup_expired_codes',
|
||||
'cleanup_expired_sessions',
|
||||
'get_database_stats',
|
||||
'DB_PATH',
|
||||
]
|
||||
591
reports-app/telegram-bot/app/db/operations.py
Normal file
591
reports-app/telegram-bot/app/db/operations.py
Normal file
@@ -0,0 +1,591 @@
|
||||
"""
|
||||
Database Operations for Telegram Bot
|
||||
|
||||
This module provides CRUD operations for:
|
||||
- telegram_users: Telegram user management and Oracle account linking
|
||||
- telegram_auth_codes: Authentication code management
|
||||
- telegram_sessions: Conversation session management
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from .database import DB_PATH
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TELEGRAM USERS OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
async def create_or_update_user(
|
||||
telegram_user_id: int,
|
||||
username: Optional[str],
|
||||
first_name: str,
|
||||
last_name: Optional[str]
|
||||
) -> bool:
|
||||
"""
|
||||
Create or update a Telegram user record.
|
||||
|
||||
Args:
|
||||
telegram_user_id: Telegram user ID
|
||||
username: Telegram username (without @)
|
||||
first_name: User's first name
|
||||
last_name: User's last name
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
INSERT INTO telegram_users (
|
||||
telegram_user_id, username, first_name, last_name, last_active_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(telegram_user_id) DO UPDATE SET
|
||||
username = excluded.username,
|
||||
first_name = excluded.first_name,
|
||||
last_name = excluded.last_name,
|
||||
last_active_at = excluded.last_active_at
|
||||
""", (telegram_user_id, username, first_name, last_name, datetime.now()))
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"User {telegram_user_id} created/updated")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create/update user {telegram_user_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def get_user(telegram_user_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get user information by Telegram user ID.
|
||||
|
||||
Args:
|
||||
telegram_user_id: Telegram user ID
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: User data or None if not found
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM telegram_users
|
||||
WHERE telegram_user_id = ?
|
||||
""", (telegram_user_id,))
|
||||
|
||||
row = await cursor.fetchone()
|
||||
if row:
|
||||
return dict(row)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get user {telegram_user_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def link_user_to_oracle(
|
||||
telegram_user_id: int,
|
||||
oracle_username: str,
|
||||
jwt_token: str,
|
||||
jwt_refresh_token: str,
|
||||
token_expires_at: datetime
|
||||
) -> bool:
|
||||
"""
|
||||
Link a Telegram user to an Oracle account and save JWT tokens.
|
||||
|
||||
Args:
|
||||
telegram_user_id: Telegram user ID
|
||||
oracle_username: Oracle username
|
||||
jwt_token: JWT access token
|
||||
jwt_refresh_token: JWT refresh token
|
||||
token_expires_at: Token expiration timestamp
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
UPDATE telegram_users
|
||||
SET oracle_username = ?,
|
||||
jwt_token = ?,
|
||||
jwt_refresh_token = ?,
|
||||
token_expires_at = ?,
|
||||
linked_at = ?,
|
||||
is_active = 1
|
||||
WHERE telegram_user_id = ?
|
||||
""", (
|
||||
oracle_username,
|
||||
jwt_token,
|
||||
jwt_refresh_token,
|
||||
token_expires_at,
|
||||
datetime.now(),
|
||||
telegram_user_id
|
||||
))
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"User {telegram_user_id} linked to Oracle user {oracle_username}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to link user {telegram_user_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def update_user_tokens(
|
||||
telegram_user_id: int,
|
||||
jwt_token: str,
|
||||
jwt_refresh_token: str,
|
||||
token_expires_at: datetime
|
||||
) -> bool:
|
||||
"""
|
||||
Update JWT tokens for a user.
|
||||
|
||||
Args:
|
||||
telegram_user_id: Telegram user ID
|
||||
jwt_token: New JWT access token
|
||||
jwt_refresh_token: New JWT refresh token
|
||||
token_expires_at: New token expiration timestamp
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
UPDATE telegram_users
|
||||
SET jwt_token = ?,
|
||||
jwt_refresh_token = ?,
|
||||
token_expires_at = ?
|
||||
WHERE telegram_user_id = ?
|
||||
""", (jwt_token, jwt_refresh_token, token_expires_at, telegram_user_id))
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Tokens updated for user {telegram_user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update tokens for user {telegram_user_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def update_user_last_active(telegram_user_id: int) -> bool:
|
||||
"""
|
||||
Update the last active timestamp for a user.
|
||||
|
||||
Args:
|
||||
telegram_user_id: Telegram user ID
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
UPDATE telegram_users
|
||||
SET last_active_at = ?
|
||||
WHERE telegram_user_id = ?
|
||||
""", (datetime.now(), telegram_user_id))
|
||||
|
||||
await db.commit()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update last active for user {telegram_user_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def is_user_linked(telegram_user_id: int) -> bool:
|
||||
"""
|
||||
Check if a user is linked to an Oracle account.
|
||||
|
||||
Args:
|
||||
telegram_user_id: Telegram user ID
|
||||
|
||||
Returns:
|
||||
bool: True if user is linked
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT oracle_username FROM telegram_users
|
||||
WHERE telegram_user_id = ? AND oracle_username IS NOT NULL
|
||||
""", (telegram_user_id,))
|
||||
|
||||
row = await cursor.fetchone()
|
||||
return row is not None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check if user {telegram_user_id} is linked: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# AUTHENTICATION CODES OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
async def create_auth_code(
|
||||
code: str,
|
||||
telegram_user_id: int,
|
||||
oracle_username: str,
|
||||
expires_in_minutes: int = 5
|
||||
) -> bool:
|
||||
"""
|
||||
Create a new authentication code for linking.
|
||||
|
||||
Args:
|
||||
code: 8-character authentication code
|
||||
telegram_user_id: Telegram user ID
|
||||
oracle_username: Oracle username to link
|
||||
expires_in_minutes: Code expiration time in minutes (default: 5)
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
try:
|
||||
expires_at = datetime.now() + timedelta(minutes=expires_in_minutes)
|
||||
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
INSERT INTO telegram_auth_codes (
|
||||
code, telegram_user_id, oracle_username, expires_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (code, telegram_user_id, oracle_username, expires_at))
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Auth code created for user {telegram_user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create auth code: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def get_auth_code(code: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get authentication code information.
|
||||
|
||||
Args:
|
||||
code: 8-character authentication code
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: Code data or None if not found
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM telegram_auth_codes
|
||||
WHERE code = ?
|
||||
""", (code,))
|
||||
|
||||
row = await cursor.fetchone()
|
||||
if row:
|
||||
return dict(row)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get auth code: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def verify_and_use_auth_code(code: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Verify an authentication code and mark it as used.
|
||||
|
||||
Args:
|
||||
code: 8-character authentication code
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: Code data if valid, None if invalid/expired
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
# Check if code exists, is not used, and not expired
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM telegram_auth_codes
|
||||
WHERE code = ?
|
||||
AND used = 0
|
||||
AND expires_at > ?
|
||||
""", (code, datetime.now()))
|
||||
|
||||
row = await cursor.fetchone()
|
||||
if not row:
|
||||
logger.warning(f"Invalid or expired code: {code}")
|
||||
return None
|
||||
|
||||
# Mark code as used
|
||||
await db.execute("""
|
||||
UPDATE telegram_auth_codes
|
||||
SET used = 1, used_at = ?
|
||||
WHERE code = ?
|
||||
""", (datetime.now(), code))
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Auth code {code} verified and used")
|
||||
|
||||
return dict(row)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to verify auth code: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def get_pending_codes_for_user(telegram_user_id: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all pending (unused, non-expired) codes for a user.
|
||||
|
||||
Args:
|
||||
telegram_user_id: Telegram user ID
|
||||
|
||||
Returns:
|
||||
List[Dict]: List of pending codes
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM telegram_auth_codes
|
||||
WHERE telegram_user_id = ?
|
||||
AND used = 0
|
||||
AND expires_at > ?
|
||||
ORDER BY created_at DESC
|
||||
""", (telegram_user_id, datetime.now()))
|
||||
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get pending codes for user {telegram_user_id}: {e}")
|
||||
return []
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SESSION OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
async def create_session(
|
||||
telegram_user_id: int,
|
||||
conversation_state: Optional[str] = None,
|
||||
expires_in_hours: int = 24
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Create a new conversation session.
|
||||
|
||||
Args:
|
||||
telegram_user_id: Telegram user ID
|
||||
conversation_state: JSON string of conversation state
|
||||
expires_in_hours: Session expiration time in hours (default: 24)
|
||||
|
||||
Returns:
|
||||
Optional[str]: Session ID if successful, None otherwise
|
||||
"""
|
||||
try:
|
||||
session_id = str(uuid.uuid4())
|
||||
expires_at = datetime.now() + timedelta(hours=expires_in_hours)
|
||||
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
INSERT INTO telegram_sessions (
|
||||
session_id, telegram_user_id, conversation_state, expires_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (session_id, telegram_user_id, conversation_state, expires_at))
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Session {session_id} created for user {telegram_user_id}")
|
||||
return session_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create session: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def get_session(session_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get session information.
|
||||
|
||||
Args:
|
||||
session_id: Session UUID
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: Session data or None if not found/expired
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM telegram_sessions
|
||||
WHERE session_id = ?
|
||||
AND expires_at > ?
|
||||
""", (session_id, datetime.now()))
|
||||
|
||||
row = await cursor.fetchone()
|
||||
if row:
|
||||
return dict(row)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get session {session_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def get_user_active_session(telegram_user_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get the most recent active session for a user.
|
||||
|
||||
Args:
|
||||
telegram_user_id: Telegram user ID
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: Session data or None if no active session
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM telegram_sessions
|
||||
WHERE telegram_user_id = ?
|
||||
AND expires_at > ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1
|
||||
""", (telegram_user_id, datetime.now()))
|
||||
|
||||
row = await cursor.fetchone()
|
||||
if row:
|
||||
return dict(row)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get active session for user {telegram_user_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def update_session_state(
|
||||
session_id: str,
|
||||
conversation_state: str
|
||||
) -> bool:
|
||||
"""
|
||||
Update the conversation state for a session.
|
||||
|
||||
Args:
|
||||
session_id: Session UUID
|
||||
conversation_state: JSON string of conversation state
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
UPDATE telegram_sessions
|
||||
SET conversation_state = ?,
|
||||
updated_at = ?
|
||||
WHERE session_id = ?
|
||||
""", (conversation_state, datetime.now(), session_id))
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Session {session_id} state updated")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update session {session_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def delete_session(session_id: str) -> bool:
|
||||
"""
|
||||
Delete a session.
|
||||
|
||||
Args:
|
||||
session_id: Session UUID
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
DELETE FROM telegram_sessions
|
||||
WHERE session_id = ?
|
||||
""", (session_id,))
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Session {session_id} deleted")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete session {session_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def delete_user_sessions(telegram_user_id: int) -> bool:
|
||||
"""
|
||||
Delete all sessions for a user.
|
||||
|
||||
Args:
|
||||
telegram_user_id: Telegram user ID
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
DELETE FROM telegram_sessions
|
||||
WHERE telegram_user_id = ?
|
||||
""", (telegram_user_id,))
|
||||
|
||||
await db.commit()
|
||||
deleted = cursor.rowcount
|
||||
logger.info(f"Deleted {deleted} sessions for user {telegram_user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete sessions for user {telegram_user_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# Export all functions
|
||||
__all__ = [
|
||||
# User operations
|
||||
'create_or_update_user',
|
||||
'get_user',
|
||||
'link_user_to_oracle',
|
||||
'update_user_tokens',
|
||||
'update_user_last_active',
|
||||
'is_user_linked',
|
||||
# Auth code operations
|
||||
'create_auth_code',
|
||||
'get_auth_code',
|
||||
'verify_and_use_auth_code',
|
||||
'get_pending_codes_for_user',
|
||||
# Session operations
|
||||
'create_session',
|
||||
'get_session',
|
||||
'get_user_active_session',
|
||||
'update_session_state',
|
||||
'delete_session',
|
||||
'delete_user_sessions',
|
||||
]
|
||||
Reference in New Issue
Block a user