переделал систему подключения агента к веб-сокету

This commit is contained in:
2026-04-23 14:24:21 +03:00
parent bb1019f816
commit c03b767979
5 changed files with 56 additions and 144 deletions

156
app.py
View File

@@ -16,11 +16,8 @@ import json
from pathlib import Path
import io
import asyncio
import contextlib
import traceback
import time
from urllib.parse import quote
from db import get_connection
from repositories.match_formation_repository import (
get_match_formations,
@@ -29,9 +26,6 @@ from repositories.match_formation_repository import (
)
from parsers.parser_game import run_parser_game
from parsers.parser_players import run_parser_players
from parsers.parser_schedule import run_parser_schedule
from parsers.parser_standings import run_parser_standings
from repositories.match_session_repository import (
create_match_session,
get_match_session_by_token,
@@ -91,7 +85,6 @@ from repositories.match_event_repository import (
from repositories.auth_repository import get_user_by_username
from services.auth_service import (
create_auth_session,
hash_password,
verify_password,
get_current_user_from_request,
build_not_authenticated_response,
@@ -128,6 +121,7 @@ class PublishVmixCommandPayload(BaseModel):
commands: List[str] = Field(default_factory=list)
target_client_id: str | None = None
target_group: str | None = None
target_login: str | None = None
meta: dict | None = None
@@ -242,6 +236,7 @@ class VmixConnectionManager:
message: dict,
target_group: str | None = None,
session_token: str | None = None,
target_login: str | None = None,
):
async with self.lock:
items = list(self.connections.values())
@@ -252,6 +247,8 @@ class VmixConnectionManager:
continue
if target_group and conn.get("group_name") != target_group:
continue
if target_login and conn.get("operator_name") != target_login:
continue
if session_token and conn.get("session_token") != session_token:
continue
matched.append(conn["client_id"])
@@ -578,13 +575,20 @@ def download_vmix_project_page(request: Request, session_token: str):
@app.get("/admin/session/{session_token}/download-vmix")
def download_vmix_project(session_token: str):
def download_vmix_project(request: Request, session_token: str):
session_row = get_match_session_by_token(session_token)
if not session_row:
return JSONResponse({"error": "session_not_found"}, status_code=404)
current_user = getattr(request.state, "current_user", None) or {}
operator_login = current_user.get("username") or None
try:
vmix_bytes = build_vmix_project_bytes(session_token=session_token, match_id=session_row[1])
vmix_bytes = build_vmix_project_bytes(
session_token=session_token,
match_id=session_row[1],
operator_login=operator_login,
)
filename = build_vmix_filename(session_row)
return StreamingResponse(
@@ -732,143 +736,17 @@ def close_session(session_token: str):
return RedirectResponse(url="/admin/matches", status_code=303)
def render_admin_db_index(
request: Request,
parser_output: str | None = None,
parser_name: str | None = None,
parser_success: bool | None = None,
account_output: str | None = None,
account_success: bool | None = None,
):
@app.get("/admin/db", response_class=HTMLResponse)
def admin_db_index(request: Request):
return templates.TemplateResponse(
name="admin_db_index.html",
request=request,
context={
"current_user": getattr(request.state, "current_user", None),
"parser_output": parser_output,
"parser_name": parser_name,
"parser_success": parser_success,
"account_output": account_output,
"account_success": account_success,
},
)
@app.get("/admin/db", response_class=HTMLResponse)
def admin_db_index(request: Request):
return render_admin_db_index(request)
@app.post("/admin/db/run-parser", response_class=HTMLResponse)
def admin_db_run_parser(request: Request, parser_name: str = Form(...)):
current_user = getattr(request.state, "current_user", None)
if not current_user or current_user.get("role") != "admin":
return RedirectResponse(url="/login", status_code=303)
parser_map = {
"players": ("Игроки", run_parser_players),
"schedule": ("Расписание", run_parser_schedule),
"standings": ("Турнирка", run_parser_standings),
}
parser_meta = parser_map.get(parser_name)
if not parser_meta:
return render_admin_db_index(
request,
parser_output="Неизвестный парсер.",
parser_name=parser_name,
parser_success=False,
)
title, parser_func = parser_meta
buffer = io.StringIO()
success = True
with contextlib.redirect_stdout(buffer), contextlib.redirect_stderr(buffer):
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Запуск парсера: {title}")
try:
parser_func()
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Парсер завершён успешно")
except Exception:
success = False
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Ошибка при выполнении парсера")
print(traceback.format_exc())
output = buffer.getvalue().strip() or "Парсер не вернул сообщений."
return render_admin_db_index(
request,
parser_output=output,
parser_name=title,
parser_success=success,
)
@app.post("/admin/db/create-account", response_class=HTMLResponse)
def admin_db_create_account(
request: Request,
username: str = Form(...),
password: str = Form(...),
role: str = Form(default="operator"),
):
current_user = getattr(request.state, "current_user", None)
if not current_user or current_user.get("role") != "admin":
return RedirectResponse(url="/login", status_code=303)
username = username.strip()
role = (role or "operator").strip().lower()
if not username:
return render_admin_db_index(request, account_output="Логин обязателен.", account_success=False)
if not password:
return render_admin_db_index(request, account_output="Пароль обязателен.", account_success=False)
if len(password) < 6:
return render_admin_db_index(request, account_output="Пароль должен быть не короче 6 символов.", account_success=False)
if role not in {"admin", "operator"}:
return render_admin_db_index(request, account_output="Роль должна быть admin или operator.", account_success=False)
password_hash = hash_password(password)
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO admin_users (username, password_hash, role, is_active)
VALUES (%s, %s, %s, TRUE)
ON CONFLICT (username) DO NOTHING
RETURNING id;
""",
(username, password_hash, role),
)
row = cur.fetchone()
conn.commit()
if row:
return render_admin_db_index(
request,
account_output=f"Пользователь создан: {username} (id={row[0]})\nРоль: {role}",
account_success=True,
)
return render_admin_db_index(
request,
account_output=f"Пользователь '{username}' уже существует.",
account_success=False,
)
except Exception:
conn.rollback()
return render_admin_db_index(
request,
account_output="Ошибка при создании аккаунта:\n" + traceback.format_exc(),
account_success=False,
)
finally:
conn.close()
@app.get("/admin/db/players", response_class=HTMLResponse)
def admin_db_players(request: Request, q: str = Query(default="")):
# denied = require_role(request, {"admin"})
@@ -1700,6 +1578,7 @@ async def ws_vmix_client(
"client_id": client_id,
"match_id": match_id,
"session_token": session_token,
"operator_name": operator_name,
})
while True:
@@ -1748,6 +1627,7 @@ async def publish_vmix_command(payload: PublishVmixCommandPayload, request: Requ
"match_id": payload.match_id,
"session_token": payload.session_token,
"commands": commands,
"target_login": payload.target_login,
"meta": payload.meta or {},
"sent_at": time.time(),
}
@@ -1761,6 +1641,7 @@ async def publish_vmix_command(payload: PublishVmixCommandPayload, request: Requ
message=message,
target_group=payload.target_group,
session_token=payload.session_token,
target_login=payload.target_login,
)
log_action(
@@ -1773,6 +1654,7 @@ async def publish_vmix_command(payload: PublishVmixCommandPayload, request: Requ
details={
"target_client_id": payload.target_client_id,
"target_group": payload.target_group,
"target_login": payload.target_login,
"commands_count": len(commands),
"results": results,
},