- 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>
124 lines
4.1 KiB
Python
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()
|