cm_bot_v2/app/db.py
yiekheng 45303d00aa Refactor: externalize all hardcoded config to env vars, add multi-deployment support
- Remove all hardcoded credentials and config from Python source code:
  - db.py: DB host/user/password/name/port → env vars with connection retry support
  - cm_bot_hal.py: prefix, agent_id, agent_password, security_pin → env vars
  - cm_bot.py: base_url → env var, fix register_user return values
  - cm_web_view.py: hardcoded '13c' prefix → configurable CM_PREFIX_PATTERN
  - cm_telegram.py: hardcoded 'Sky533535' pin → env var CM_SECURITY_PIN

- Parameterize docker-compose.yml for multi-deployment on same host:
  - Container names use ${CM_DEPLOY_NAME} prefix (e.g. rex-cm-*, siong-cm-*)
  - Network name uses ${CM_DEPLOY_NAME}-network
  - Web view port configurable via ${CM_WEB_HOST_PORT}
  - All service config passed as env vars (not baked into image)

- Add per-deployment env configs:
  - envs/rex/.env (port 8001, prefix 13c, DB rex_cm)
  - envs/siong/.env (port 8005, prefix 13sa, DB siong_cm)
  - .env.example as template for new deployments
  - Remove .env from .gitignore (local server, safe to commit)

- Improve telegram bot reliability:
  - Add retry logic for polling with exponential backoff
  - Add error handlers for Conflict, RetryAfter, NetworkError, TimedOut
  - Add /9 command to show chat ID
  - Add telegram_notifier.py for alert notifications
  - Fix error handling in /2 and /3 command handlers

- Fix db.py cursor cleanup (close cursor before connection in finally blocks)
- Fix docker-compose.override.yml environment syntax (list → mapping)
- Update README with multi-deployment instructions
- Add AGENTS.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 22:25:40 +08:00

124 lines
4.1 KiB
Python

import os
import time
import mysql.connector
from mysql.connector import Error
def _get_required_env(name: str) -> str:
value = os.getenv(name)
if value is None or value == "":
raise RuntimeError(f"Missing required environment variable: {name}")
return value
class DB:
def __init__(self):
self.config = {
'host': _get_required_env('DB_HOST'),
'user': _get_required_env('DB_USER'),
'password': _get_required_env('DB_PASSWORD'),
'database': _get_required_env('DB_NAME'),
'port': int(_get_required_env('DB_PORT')),
'connection_timeout': int(_get_required_env('DB_CONNECTION_TIMEOUT'))
}
self.connect_retries = max(1, int(_get_required_env('DB_CONNECT_RETRIES')))
self.connect_retry_delay = float(_get_required_env('DB_CONNECT_RETRY_DELAY'))
self.init_database()
def get_connection(self):
"""Get MySQL database connection."""
for attempt in range(1, self.connect_retries + 1):
try:
connection = mysql.connector.connect(**self.config)
return connection
except Error as e:
print(f"Error connecting to MySQL: {e}")
if attempt < self.connect_retries:
print(
f"Retrying MySQL connection ({attempt}/{self.connect_retries}) "
f"in {self.connect_retry_delay} seconds..."
)
time.sleep(self.connect_retry_delay)
return None
def init_database(self):
"""Initialize the database connection."""
connection = self.get_connection()
if connection is None:
raise Exception("Failed to connect to database")
cursor = None
try:
cursor = connection.cursor()
# Test connection by checking if required tables exist
cursor.execute("SHOW TABLES LIKE 'acc'")
if not cursor.fetchone():
raise Exception("Table 'acc' does not exist")
cursor.execute("SHOW TABLES LIKE 'user'")
if not cursor.fetchone():
raise Exception("Table 'user' does not exist")
# print("Database connection verified - required tables exist")
except Error as e:
print(f"Error verifying database: {e}")
raise Exception(f"Database verification failed: {e}")
finally:
if cursor is not None:
cursor.close()
if connection.is_connected():
connection.close()
def query(self, query, params=None):
"""Execute a query and return results."""
connection = self.get_connection()
if connection is None:
return []
cursor = None
try:
cursor = connection.cursor(dictionary=True)
if params:
cursor.execute(query, params)
else:
cursor.execute(query)
results = cursor.fetchall()
return results
except Error as e:
print(f"Error executing query: {e}")
return []
finally:
if cursor is not None:
cursor.close()
if connection.is_connected():
connection.close()
def execute(self, query, params=None):
"""Execute a query that modifies data (INSERT, UPDATE, DELETE) and return success status."""
connection = self.get_connection()
if connection is None:
return False
cursor = None
try:
cursor = connection.cursor()
if params:
cursor.execute(query, params)
else:
cursor.execute(query)
connection.commit()
return True
except Error as e:
print(f"Error executing query: {e}")
return False
finally:
if cursor is not None:
cursor.close()
if connection.is_connected():
connection.close()