cm_bot_v2/app/telegram_notifier.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

116 lines
4.0 KiB
Python

import logging
import os
from pathlib import Path
from typing import Optional
import requests
logger = logging.getLogger(__name__)
TELEGRAM_API_URL = "https://api.telegram.org"
def _load_env_from_file() -> None:
"""
Load key=value pairs from a .env-style file to populate os.environ.
Existing environment variables take precedence.
"""
candidate_paths = []
env_file = os.getenv("ENV_FILE_PATH")
if env_file:
candidate_paths.append(Path(env_file))
candidate_paths.append(Path(__file__).resolve().parents[1] / ".env")
candidate_paths.append(Path.cwd() / ".env")
for path in candidate_paths:
if not path:
continue
if not path.exists() or not path.is_file():
continue
try:
with path.open("r") as env_file_handle:
for line in env_file_handle:
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
continue
key, value = stripped.split("=", 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
if key and key not in os.environ:
os.environ[key] = value
except OSError as exc:
logger.warning("Unable to load env file %s: %s", path, exc)
_load_env_from_file()
def get_telegram_bot_token() -> str:
"""
Fetch the Telegram bot token from the TELEGRAM_BOT_TOKEN environment variable.
Raises RuntimeError if missing so the application fails fast during startup.
"""
token = os.getenv("TELEGRAM_BOT_TOKEN")
if not token:
_load_env_from_file()
token = os.getenv("TELEGRAM_BOT_TOKEN")
if not token:
raise RuntimeError("Missing TELEGRAM_BOT_TOKEN environment variable")
return token
class TelegramNotifier:
"""
Utility wrapper to send proactive notifications to a dedicated Telegram chat.
Requires TELEGRAM_ALERT_CHAT_ID and TELEGRAM_ALERT_BOT_TOKEN (or TELEGRAM_BOT_TOKEN) env vars.
"""
def __init__(self):
self.chat_id = os.getenv("TELEGRAM_ALERT_CHAT_ID")
self.bot_token = os.getenv("TELEGRAM_ALERT_BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN")
if not self.chat_id:
logger.warning(
"TELEGRAM_ALERT_CHAT_ID environment variable missing; skipping Telegram notification setup"
)
if not self.bot_token:
logger.warning(
"TELEGRAM_ALERT_BOT_TOKEN/TELEGRAM_BOT_TOKEN env var missing; skipping Telegram notification setup"
)
def _can_notify(self) -> bool:
return bool(self.chat_id and self.bot_token)
def _send_message(self, text: str, parse_mode: Optional[str] = None) -> None:
if not self._can_notify():
logger.warning(
"Skipping Telegram notification: bot_token/chat_id not configured. Message: %s",
text,
)
return
url = f"{TELEGRAM_API_URL}/bot{self.bot_token}/sendMessage"
payload = {"chat_id": self.chat_id, "text": text}
if parse_mode:
payload["parse_mode"] = parse_mode
try:
response = requests.post(url, json=payload, timeout=10)
if response.status_code != 200:
logger.error(
"Failed to send Telegram notification. Status: %s Body: %s",
response.status_code,
response.text,
)
except requests.RequestException as exc:
logger.exception("Error sending Telegram notification: %s", exc)
def notify_login_failure(self, details: str) -> None:
message = f"⚠️ CM Telegram Bot login issue detected.\nDetails: {details}\nAutomatic account creation paused until the next retry window."
self._send_message(message)
def notify_generic_error(self, details: str) -> None:
message = f"⚠️ CM Telegram Bot encountered an issue:\n{details}"
self._send_message(message)