2026-05-20 17:02:45 +08:00
|
|
|
"""Nexus — Telegram Bot Notification Module
|
|
|
|
|
Sends alerts/recovery/system messages via Telegram Bot API.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
import httpx
|
2026-05-21 22:11:38 +08:00
|
|
|
from datetime import datetime, timezone
|
2026-05-20 17:02:45 +08:00
|
|
|
|
|
|
|
|
from server.config import settings
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("nexus.telegram")
|
|
|
|
|
|
|
|
|
|
# Bot API base URL
|
|
|
|
|
TELEGRAM_API_BASE = "https://api.telegram.org"
|
|
|
|
|
|
2026-05-22 08:19:56 +08:00
|
|
|
# Reusable httpx client — avoids TCP connection churn during alert storms
|
|
|
|
|
_http_client: httpx.AsyncClient | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _get_client() -> httpx.AsyncClient:
|
|
|
|
|
global _http_client
|
|
|
|
|
if _http_client is None or _http_client.is_closed:
|
|
|
|
|
_http_client = httpx.AsyncClient(timeout=10.0)
|
|
|
|
|
return _http_client
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def close_client():
|
|
|
|
|
"""Close the shared httpx client — call during app shutdown"""
|
|
|
|
|
global _http_client
|
|
|
|
|
if _http_client and not _http_client.is_closed:
|
|
|
|
|
await _http_client.aclose()
|
|
|
|
|
_http_client = None
|
|
|
|
|
|
2026-05-20 17:02:45 +08:00
|
|
|
|
|
|
|
|
async def send_telegram(message: str) -> bool:
|
|
|
|
|
"""Send a Telegram message via Bot API.
|
|
|
|
|
|
|
|
|
|
Returns True if sent successfully, False if skipped or failed.
|
|
|
|
|
Silently skips if TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID is not configured.
|
|
|
|
|
"""
|
|
|
|
|
if not settings.TELEGRAM_BOT_TOKEN or not settings.TELEGRAM_CHAT_ID:
|
|
|
|
|
return False # Not configured — silent skip
|
|
|
|
|
|
|
|
|
|
try:
|
2026-05-22 08:19:56 +08:00
|
|
|
client = await _get_client()
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
f"{TELEGRAM_API_BASE}/bot{settings.TELEGRAM_BOT_TOKEN}/sendMessage",
|
|
|
|
|
json={
|
|
|
|
|
"chat_id": settings.TELEGRAM_CHAT_ID,
|
|
|
|
|
"text": message,
|
|
|
|
|
"parse_mode": "HTML",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if resp.status_code == 200:
|
|
|
|
|
logger.info(f"Telegram sent: {message[:50]}...")
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
logger.error(f"Telegram API error: {resp.status_code} {resp.text[:100]}")
|
|
|
|
|
return False
|
2026-05-20 17:02:45 +08:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Telegram send failed: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def send_telegram_alert(server_name: str, alert_type: str, value: float) -> bool:
|
|
|
|
|
"""Send server alert notification"""
|
2026-05-21 22:11:38 +08:00
|
|
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
2026-05-20 17:02:45 +08:00
|
|
|
return await send_telegram(
|
|
|
|
|
f"🔴 <b>告警</b>\n"
|
|
|
|
|
f"服务器 <b>{server_name}</b> {alert_type} 使用率 <b>{value:.1f}%</b>\n"
|
|
|
|
|
f"时间: {now}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def send_telegram_recovery(server_name: str, metric: str, value: float) -> bool:
|
|
|
|
|
"""Send server recovery notification"""
|
2026-05-21 22:11:38 +08:00
|
|
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
2026-05-20 17:02:45 +08:00
|
|
|
return await send_telegram(
|
|
|
|
|
f"🟢 <b>恢复</b>\n"
|
|
|
|
|
f"服务器 <b>{server_name}</b> {metric} 已恢复正常 ({value:.1f}%)\n"
|
|
|
|
|
f"时间: {now}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def send_telegram_system_alert(message: str) -> bool:
|
|
|
|
|
"""Send system-level alert (Python crash, Redis disconnect, etc.)"""
|
2026-05-21 22:11:38 +08:00
|
|
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
2026-05-20 17:02:45 +08:00
|
|
|
return await send_telegram(
|
|
|
|
|
f"⚠️ <b>系统告警</b>\n"
|
|
|
|
|
f"{message}\n"
|
|
|
|
|
f"时间: {now}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def send_telegram_restart_success() -> bool:
|
|
|
|
|
"""Notify that Python backend has successfully restarted"""
|
2026-05-21 22:11:38 +08:00
|
|
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
2026-05-20 17:02:45 +08:00
|
|
|
return await send_telegram(
|
|
|
|
|
f"🟢 <b>Nexus后端已恢复</b>\n"
|
|
|
|
|
f"Python服务自动重启成功\n"
|
|
|
|
|
f"时间: {now}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def send_telegram_restart_failed() -> bool:
|
|
|
|
|
"""Notify that Python backend restart failed — needs human intervention"""
|
2026-05-21 22:11:38 +08:00
|
|
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
2026-05-20 17:02:45 +08:00
|
|
|
return await send_telegram(
|
|
|
|
|
f"🔴 <b>Nexus后端重启失败!</b>\n"
|
|
|
|
|
f"请人工介入检查\n"
|
|
|
|
|
f"时间: {now}"
|
|
|
|
|
)
|