Files
RelayOps/bot/app/main.py
2026-06-07 02:32:28 +03:00

215 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
from dataclasses import dataclass
from typing import Any
import httpx
from pydantic_settings import BaseSettings, SettingsConfigDict
from telegram import Update
from telegram.ext import (
Application,
CommandHandler,
ContextTypes,
)
logger = logging.getLogger("server_panel.telegram_bot")
class BotSettings(BaseSettings):
telegram_bot_token: str = ""
telegram_mode: str = "polling"
backend_api_base_url: str = "http://backend:8000/api/v1"
telegram_bot_api_secret: str = ""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
class BackendApiError(Exception):
pass
@dataclass(slots=True)
class BackendClient:
base_url: str
bot_secret: str
async def post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
async with httpx.AsyncClient(timeout=15) as client:
response = await client.post(
f"{self.base_url}{path}",
json=payload,
headers={"X-Telegram-Bot-Secret": self.bot_secret},
)
try:
body = response.json()
except ValueError:
body = {}
if response.is_success:
return body
detail = body.get("detail", {}) if isinstance(body, dict) else {}
if isinstance(detail, dict):
message = detail.get("message", f"Backend returned {response.status_code}.")
else:
message = str(detail) or f"Backend returned {response.status_code}."
raise BackendApiError(message)
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if update.effective_user is None or update.effective_chat is None or update.message is None:
return
backend: BackendClient = context.application.bot_data["backend_client"]
if not context.args:
await update.message.reply_text(
"Отправь /start LINK_CODE чтобы привязать Telegram к своему аккаунту Server Panel."
)
return
link_code = context.args[0].strip()
try:
await backend.post(
"/telegram/bot/link/complete",
{
"link_code": link_code,
"telegram_user_id": str(update.effective_user.id),
"chat_id": str(update.effective_chat.id),
},
)
except BackendApiError as exc:
await update.message.reply_text(f"Не удалось завершить привязку: {exc}")
return
await update.message.reply_text(
"Привязка завершена. Теперь доступны /status, /services и /restart."
)
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if update.effective_user is None or update.message is None:
return
backend: BackendClient = context.application.bot_data["backend_client"]
try:
payload = await backend.post(
"/telegram/bot/status",
{"telegram_user_id": str(update.effective_user.id)},
)
except BackendApiError as exc:
await update.message.reply_text(f"Не удалось получить статус: {exc}")
return
lines = [f"Пользователь: {payload['username']} ({payload['role']})"]
for server in payload["servers"]:
marker = "OK" if server["reachable"] else "DOWN"
lines.append(
f"{marker} {server['name']} [{server['environment']}] - {server['status']}"
)
await update.message.reply_text("\n".join(lines))
async def services_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if update.effective_user is None or update.message is None:
return
backend: BackendClient = context.application.bot_data["backend_client"]
server_name = " ".join(context.args).strip() or None
try:
payload = await backend.post(
"/telegram/bot/services",
{
"telegram_user_id": str(update.effective_user.id),
"server_name": server_name,
},
)
except BackendApiError as exc:
await update.message.reply_text(f"Не удалось получить сервисы: {exc}")
return
if not payload["services"]:
await update.message.reply_text("Сервисы не найдены.")
return
lines = [f"Пользователь: {payload['username']} ({payload['role']})"]
for service in payload["services"]:
lines.append(
f"{service['server_name']}: {service['service_name']} [{service['service_type']}] - {service['status']}"
)
await update.message.reply_text("\n".join(lines))
async def restart_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if update.effective_user is None or update.message is None:
return
if len(context.args) < 2:
await update.message.reply_text("Использование: /restart SERVER_NAME SERVICE_NAME")
return
backend: BackendClient = context.application.bot_data["backend_client"]
server_name = context.args[0].strip()
service_name = context.args[1].strip()
try:
payload = await backend.post(
"/telegram/bot/restart",
{
"telegram_user_id": str(update.effective_user.id),
"server_name": server_name,
"service_name": service_name,
},
)
except BackendApiError as exc:
await update.message.reply_text(f"Не удалось запустить restart: {exc}")
return
# The backend keeps the real execution state, while the bot only relays the
# action ID and initial queue status back to the operator.
await update.message.reply_text(
f"Restart поставлен в очередь: action #{payload['id']} status={payload['status']}"
)
def configure_logging() -> None:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
def build_application(settings: BotSettings) -> Application:
application = Application.builder().token(settings.telegram_bot_token).build()
application.bot_data["backend_client"] = BackendClient(
base_url=settings.backend_api_base_url,
bot_secret=settings.telegram_bot_api_secret,
)
application.add_handler(CommandHandler("start", start_command))
application.add_handler(CommandHandler("status", status_command))
application.add_handler(CommandHandler("services", services_command))
application.add_handler(CommandHandler("restart", restart_command))
return application
def main() -> None:
configure_logging()
settings = BotSettings()
if not settings.telegram_bot_token:
print("Telegram bot scaffold is configured, but TELEGRAM_BOT_TOKEN is empty.")
return
if not settings.telegram_bot_api_secret:
print("Telegram bot scaffold is configured, but TELEGRAM_BOT_API_SECRET is empty.")
return
logger.info("Starting Telegram bot in %s mode.", settings.telegram_mode)
application = build_application(settings)
application.run_polling()
if __name__ == "__main__":
main()