Remove web/ directory (TSS API now in tssbot.web-backend)
This commit is contained in:
@@ -1 +0,0 @@
|
||||
FOR TSSBOT WEBSITE, READ ~/GitHub/tssbot.web OR on server ~/tssbot.web, clippi is fucking annoying with this shit
|
||||
@@ -1,60 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
from web.api import db
|
||||
|
||||
ENDPOINTS = [
|
||||
"/api/tss/info", "/api/tss/live", "/api/tss/player/{uid}",
|
||||
"/api/tss/player/{uid}/games", "/api/tss/player/{uid}/history",
|
||||
"/api/tss/search/{nick}", "/api/tss/match/{session_id}",
|
||||
"/api/tss/match/{session_id}/scoreboard", "/api/tss/matches/search",
|
||||
"/api/tss/maps", "/api/tss/leaderboard/players",
|
||||
"/api/tss/leaderboard/vehicles", "/api/tss/leaderboard/stats",
|
||||
"/api/tss/tournaments", "/api/tss/tournament/{id}",
|
||||
"/api/tss/tournament/{id}/standings", "/api/tss/tournament/{id}/matches",
|
||||
]
|
||||
|
||||
info_router = APIRouter()
|
||||
|
||||
|
||||
@info_router.get("/api/tss/info")
|
||||
async def info() -> dict:
|
||||
matches = await db.query_one(db.battles_path(), "SELECT COUNT(*) c FROM match_summary")
|
||||
pg = await db.query_one(db.battles_path(), "SELECT COUNT(*) c FROM player_games_hist")
|
||||
tn = await db.query_one(db.tournaments_path(), "SELECT COUNT(*) c FROM tournaments")
|
||||
return {
|
||||
"service": "tss",
|
||||
"counts": {
|
||||
"matches": matches["c"] if matches else 0,
|
||||
"player_games": pg["c"] if pg else 0,
|
||||
"tournaments": tn["c"] if tn else 0,
|
||||
},
|
||||
"endpoints": ENDPOINTS,
|
||||
}
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
app = FastAPI(title="TSS HTTP API", version="1.0.0")
|
||||
|
||||
@app.exception_handler(StarletteHTTPException)
|
||||
async def _err(_: Request, exc: StarletteHTTPException) -> JSONResponse:
|
||||
detail = exc.detail
|
||||
body = detail if isinstance(detail, dict) else {"error": str(detail)}
|
||||
return JSONResponse(status_code=exc.status_code, content=body)
|
||||
|
||||
app.include_router(info_router)
|
||||
from web.api.routes_players import router as players_router
|
||||
from web.api.routes_matches import router as matches_router
|
||||
from web.api.routes_leaderboard import router as lb_router
|
||||
from web.api.routes_tournaments import router as tour_router
|
||||
app.include_router(players_router)
|
||||
app.include_router(matches_router)
|
||||
app.include_router(lb_router)
|
||||
app.include_router(tour_router)
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
@@ -1,47 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import aiosqlite
|
||||
|
||||
# Make TSSBOT root importable for BOT.storage defaults.
|
||||
_TSSBOT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(_TSSBOT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_TSSBOT_ROOT))
|
||||
|
||||
|
||||
def battles_path() -> Path:
|
||||
override = os.getenv("TSS_API_BATTLES_DB", "").strip()
|
||||
if override:
|
||||
return Path(override)
|
||||
from BOT.storage import TSS_BATTLES_DB_PATH
|
||||
return TSS_BATTLES_DB_PATH
|
||||
|
||||
|
||||
def tournaments_path() -> Path:
|
||||
override = os.getenv("TSS_API_TOURNAMENTS_DB", "").strip()
|
||||
if override:
|
||||
return Path(override)
|
||||
from BOT.storage import STORAGE_DIR
|
||||
return STORAGE_DIR / "tss_tournaments.db"
|
||||
|
||||
|
||||
def _ro_uri(path: Path) -> str:
|
||||
return f"file:{path}?mode=ro"
|
||||
|
||||
|
||||
async def query(path: Path, sql: str, params: tuple[Any, ...] = ()) -> list[dict]:
|
||||
async with aiosqlite.connect(_ro_uri(path), uri=True) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
async with conn.execute(sql, params) as cur:
|
||||
rows = await cur.fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def query_one(path: Path, sql: str, params: tuple[Any, ...] = ()) -> dict | None:
|
||||
rows = await query(path, sql, params)
|
||||
return rows[0] if rows else None
|
||||
@@ -1,69 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from web.api import db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _window(start_date, end_date, tournament_id):
|
||||
"""Return (where_sql, params) or raise 400 if no filter given."""
|
||||
clauses: list[str] = []
|
||||
params: list = []
|
||||
if tournament_id is not None:
|
||||
clauses.append("p.session_id IN (SELECT session_id FROM match_summary WHERE tournament_id = ?)")
|
||||
params.append(tournament_id)
|
||||
if start_date is not None:
|
||||
clauses.append("p.endtime_unix >= ?"); params.append(start_date)
|
||||
if end_date is not None:
|
||||
clauses.append("p.endtime_unix <= ?"); params.append(end_date)
|
||||
if not clauses:
|
||||
raise HTTPException(status_code=400, detail={
|
||||
"error": "a date window (start_date/end_date) or tournament_id is required",
|
||||
"code": "FILTER_REQUIRED"})
|
||||
return "WHERE " + " AND ".join(clauses), params
|
||||
|
||||
|
||||
@router.get("/api/tss/leaderboard/players")
|
||||
async def leaderboard_players(start_date: int | None = None, end_date: int | None = None,
|
||||
tournament_id: int | None = None, limit: int = 100) -> dict:
|
||||
where, params = _window(start_date, end_date, tournament_id)
|
||||
params.append(max(1, min(limit, 500)))
|
||||
rows = await db.query(db.battles_path(), f"""
|
||||
SELECT p.UID uid, MAX(p.nick) nick, COUNT(*) battles,
|
||||
SUM(CASE WHEN p.victor_bool='Win' THEN 1 ELSE 0 END) wins,
|
||||
SUM(p.air_kills+p.ground_kills) kills, SUM(p.deaths) deaths, SUM(p.score) score
|
||||
FROM player_games_hist p {where}
|
||||
GROUP BY p.UID ORDER BY score DESC LIMIT ?""", tuple(params))
|
||||
for r in rows:
|
||||
r["uid"] = str(r["uid"])
|
||||
return {"players": rows}
|
||||
|
||||
|
||||
@router.get("/api/tss/leaderboard/vehicles")
|
||||
async def leaderboard_vehicles(start_date: int | None = None, end_date: int | None = None,
|
||||
tournament_id: int | None = None, limit: int = 100) -> dict:
|
||||
where, params = _window(start_date, end_date, tournament_id)
|
||||
params.append(max(1, min(limit, 500)))
|
||||
rows = await db.query(db.battles_path(), f"""
|
||||
SELECT p.vehicle_internal, MAX(p.vehicle) vehicle, COUNT(*) battles,
|
||||
SUM(p.air_kills+p.ground_kills) kills, SUM(p.deaths) deaths, SUM(p.score) score
|
||||
FROM player_games_hist p {where}
|
||||
GROUP BY p.vehicle_internal ORDER BY battles DESC LIMIT ?""", tuple(params))
|
||||
return {"vehicles": rows}
|
||||
|
||||
|
||||
@router.get("/api/tss/leaderboard/stats")
|
||||
async def leaderboard_stats(start_date: int | None = None, end_date: int | None = None,
|
||||
tournament_id: int | None = None) -> dict:
|
||||
where, params = _window(start_date, end_date, tournament_id)
|
||||
totals = await db.query_one(db.battles_path(), f"""
|
||||
SELECT COUNT(*) battles, COUNT(DISTINCT p.UID) players,
|
||||
SUM(p.air_kills+p.ground_kills) kills, SUM(p.score) score
|
||||
FROM player_games_hist p {where}""", tuple(params))
|
||||
top = await db.query(db.battles_path(), f"""
|
||||
SELECT p.vehicle_internal, MAX(p.vehicle) vehicle, COUNT(*) battles
|
||||
FROM player_games_hist p {where}
|
||||
GROUP BY p.vehicle_internal ORDER BY battles DESC LIMIT 10""", tuple(params))
|
||||
return {"totals": totals or {}, "top_vehicles": top}
|
||||
@@ -1,104 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from web.api import db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/api/tss/live")
|
||||
async def live(limit: int = 50) -> dict:
|
||||
rows = await db.query(db.battles_path(),
|
||||
"SELECT * FROM match_summary ORDER BY endtime_unix DESC LIMIT ?",
|
||||
(max(1, min(limit, 200)),))
|
||||
return {"total": len(rows), "matches": rows}
|
||||
|
||||
|
||||
def _roster_team(rows: list[dict], slot: str, winning: str, losing: str, draw: int) -> dict:
|
||||
players = [r for r in rows if (r.get("team_slot") or "") == slot]
|
||||
for p in players:
|
||||
p["UID"] = str(p["UID"])
|
||||
return {
|
||||
"team_slot": slot,
|
||||
"team_name": players[0]["team_name"] if players else None,
|
||||
"team_id": players[0]["team_id"] if players else None,
|
||||
"is_winner": (not draw) and slot == winning,
|
||||
"is_loser": (not draw) and slot == losing,
|
||||
"players": players,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/tss/match/{session_id}")
|
||||
async def match(session_id: str) -> dict:
|
||||
summary = await db.query_one(db.battles_path(),
|
||||
"SELECT * FROM match_summary WHERE session_id = ?", (session_id,))
|
||||
if not summary:
|
||||
raise HTTPException(status_code=404, detail=f"match {session_id} not found")
|
||||
rows = await db.query(db.battles_path(),
|
||||
"SELECT * FROM player_games_hist WHERE session_id = ?", (session_id,))
|
||||
slots = sorted({(r.get("team_slot") or "") for r in rows if r.get("team_slot")})
|
||||
teams = [_roster_team(rows, s, summary["winning_slot"], summary["losing_slot"],
|
||||
summary["draw"]) for s in slots]
|
||||
return {"match": summary, "teams": teams}
|
||||
|
||||
|
||||
@router.get("/api/tss/match/{session_id}/scoreboard")
|
||||
async def scoreboard(session_id: str) -> dict:
|
||||
base = await match(session_id) # reuses 404 + roster logic
|
||||
logs_row = await db.query_one(db.battles_path(),
|
||||
"SELECT chat_log_json, battle_log_json, event_log_json FROM match_logs WHERE session_id = ?",
|
||||
(session_id,))
|
||||
if logs_row:
|
||||
def _parse(v):
|
||||
try:
|
||||
return json.loads(v) if v else None
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return None
|
||||
logs = {"available": True,
|
||||
"chat": _parse(logs_row["chat_log_json"]),
|
||||
"battle": _parse(logs_row["battle_log_json"]),
|
||||
"events": _parse(logs_row["event_log_json"])}
|
||||
else:
|
||||
logs = {"available": False}
|
||||
return {**base, "logs": logs}
|
||||
|
||||
|
||||
@router.get("/api/tss/matches/search")
|
||||
async def matches_search(player: str | None = None, team: str | None = None,
|
||||
mission: str | None = None, tournament: str | None = None,
|
||||
time_from: int | None = None, time_to: int | None = None,
|
||||
limit: int = 50) -> dict:
|
||||
clauses: list[str] = []
|
||||
params: list = []
|
||||
join = ""
|
||||
if player or team:
|
||||
join = "JOIN player_games_hist p ON p.session_id = m.session_id"
|
||||
if player:
|
||||
clauses.append("p.UID = ?"); params.append(player)
|
||||
if team:
|
||||
clauses.append("p.team_name = ?"); params.append(team)
|
||||
if mission:
|
||||
clauses.append("m.mission_name LIKE ?"); params.append(f"%{mission}%")
|
||||
if tournament:
|
||||
clauses.append("m.tournament_name LIKE ?"); params.append(f"%{tournament}%")
|
||||
if time_from is not None:
|
||||
clauses.append("m.endtime_unix >= ?"); params.append(time_from)
|
||||
if time_to is not None:
|
||||
clauses.append("m.endtime_unix <= ?"); params.append(time_to)
|
||||
where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
|
||||
params.append(max(1, min(limit, 200)))
|
||||
rows = await db.query(db.battles_path(),
|
||||
f"SELECT DISTINCT m.* FROM match_summary m {join} {where} "
|
||||
f"ORDER BY m.endtime_unix DESC LIMIT ?", tuple(params))
|
||||
return {"total": len(rows), "matches": rows}
|
||||
|
||||
|
||||
@router.get("/api/tss/maps")
|
||||
async def maps() -> dict:
|
||||
rows = await db.query(db.battles_path(),
|
||||
"SELECT DISTINCT mission_name, level_path FROM match_summary "
|
||||
"WHERE mission_name IS NOT NULL ORDER BY mission_name")
|
||||
return {"total": len(rows), "maps": rows}
|
||||
@@ -1,77 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from web.api import db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/api/tss/player/{uid}")
|
||||
async def player(uid: str) -> dict:
|
||||
bp = db.battles_path()
|
||||
summary = await db.query_one(bp, """
|
||||
SELECT COUNT(*) battles,
|
||||
SUM(CASE WHEN victor_bool='Win' THEN 1 ELSE 0 END) wins,
|
||||
SUM(CASE WHEN victor_bool!='Win' THEN 1 ELSE 0 END) losses,
|
||||
SUM(ground_kills) ground_kills, SUM(air_kills) air_kills,
|
||||
SUM(assists) assists, SUM(captures) captures, SUM(deaths) deaths,
|
||||
SUM(score) score, AVG(pvp_ratio) avg_pvp_ratio
|
||||
FROM player_games_hist WHERE UID = ?""", (uid,))
|
||||
if not summary or summary["battles"] == 0:
|
||||
raise HTTPException(status_code=404, detail=f"player {uid} not found")
|
||||
vehicles = await db.query(bp, """
|
||||
SELECT vehicle, vehicle_internal, COUNT(*) battles, SUM(air_kills+ground_kills) kills,
|
||||
SUM(deaths) deaths, SUM(score) score
|
||||
FROM player_games_hist WHERE UID = ?
|
||||
GROUP BY vehicle_internal ORDER BY battles DESC""", (uid,))
|
||||
nicks = await db.query(bp, """
|
||||
SELECT nick, MIN(endtime_unix) first_seen, MAX(endtime_unix) last_seen
|
||||
FROM player_games_hist WHERE UID = ? GROUP BY nick ORDER BY last_seen DESC""", (uid,))
|
||||
team_history = await db.query(bp, """
|
||||
SELECT team_id, team_name, tss_role, COUNT(*) battles,
|
||||
MIN(endtime_unix) first_seen, MAX(endtime_unix) last_seen
|
||||
FROM player_games_hist WHERE UID = ?
|
||||
GROUP BY team_id, team_name, tss_role ORDER BY last_seen DESC""", (uid,))
|
||||
win_rate = round(summary["wins"] / summary["battles"], 4) if summary["battles"] else 0.0
|
||||
return {"uid": str(uid), "summary": {**summary, "win_rate": win_rate},
|
||||
"vehicles": vehicles, "nicks": nicks, "team_history": team_history}
|
||||
|
||||
|
||||
@router.get("/api/tss/player/{uid}/games")
|
||||
async def player_games(uid: str, limit: int = 100,
|
||||
time_from: int | None = None, time_to: int | None = None) -> dict:
|
||||
clauses = ["p.UID = ?"]
|
||||
params: list = [uid]
|
||||
if time_from is not None:
|
||||
clauses.append("p.endtime_unix >= ?"); params.append(time_from)
|
||||
if time_to is not None:
|
||||
clauses.append("p.endtime_unix <= ?"); params.append(time_to)
|
||||
params.append(max(1, min(limit, 1000)))
|
||||
rows = await db.query(db.battles_path(), f"""
|
||||
SELECT p.*, m.mission_name, m.tournament_name
|
||||
FROM player_games_hist p LEFT JOIN match_summary m ON m.session_id = p.session_id
|
||||
WHERE {' AND '.join(clauses)} ORDER BY p.endtime_unix DESC LIMIT ?""", tuple(params))
|
||||
return {"uid": str(uid), "total": len(rows), "games": rows}
|
||||
|
||||
|
||||
@router.get("/api/tss/player/{uid}/history")
|
||||
async def player_history(uid: str) -> dict:
|
||||
rows = await db.query(db.battles_path(), """
|
||||
SELECT date(endtime_unix, 'unixepoch') day, COUNT(*) battles,
|
||||
SUM(CASE WHEN victor_bool='Win' THEN 1 ELSE 0 END) wins,
|
||||
SUM(air_kills+ground_kills) kills, SUM(deaths) deaths, SUM(score) score
|
||||
FROM player_games_hist WHERE UID = ? GROUP BY day ORDER BY day DESC""", (uid,))
|
||||
return {"uid": str(uid), "days_with_battles_only": True, "history": rows}
|
||||
|
||||
|
||||
@router.get("/api/tss/search/{nick}")
|
||||
async def search(nick: str, limit: int = 25) -> dict:
|
||||
rows = await db.query(db.battles_path(), """
|
||||
SELECT UID uid, nick, MAX(endtime_unix) last_seen, COUNT(*) battles
|
||||
FROM player_games_hist WHERE nick LIKE ? COLLATE NOCASE
|
||||
GROUP BY UID, nick ORDER BY last_seen DESC LIMIT ?""",
|
||||
(f"%{nick}%", max(1, min(limit, 100))))
|
||||
for r in rows:
|
||||
r["uid"] = str(r["uid"])
|
||||
return {"query": nick, "players": rows}
|
||||
@@ -1,76 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from web.api import db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _nullify(row: dict, keys: tuple[str, ...]) -> dict:
|
||||
for k in keys:
|
||||
if row.get(k) == "":
|
||||
row[k] = None
|
||||
return row
|
||||
|
||||
|
||||
@router.get("/api/tss/tournaments")
|
||||
async def tournaments(status: str | None = None, game_mode: str | None = None,
|
||||
cluster: str | None = None, limit: int = 100) -> dict:
|
||||
clauses: list[str] = []
|
||||
params: list = []
|
||||
if status:
|
||||
clauses.append("status = ?"); params.append(status)
|
||||
if game_mode:
|
||||
clauses.append("game_mode = ?"); params.append(game_mode)
|
||||
if cluster:
|
||||
clauses.append("cluster = ?"); params.append(cluster)
|
||||
where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
|
||||
params.append(max(1, min(limit, 500)))
|
||||
rows = await db.query(db.tournaments_path(),
|
||||
f"SELECT * FROM tournaments {where} ORDER BY date_end DESC LIMIT ?", tuple(params))
|
||||
return {"total": len(rows), "tournaments": rows}
|
||||
|
||||
|
||||
async def _matches(tid: int) -> list[dict]:
|
||||
rows = await db.query(db.tournaments_path(),
|
||||
"SELECT * FROM tournament_matches WHERE tournament_id = ? ORDER BY round, position",
|
||||
(tid,))
|
||||
battles = await db.query(db.tournaments_path(),
|
||||
"SELECT match_id, type_bracket, session_hex FROM tournament_battles WHERE tournament_id = ?",
|
||||
(tid,))
|
||||
by_match: dict[tuple, list[str]] = {}
|
||||
for b in battles:
|
||||
by_match.setdefault((b["match_id"], b["type_bracket"]), []).append(b["session_hex"])
|
||||
out = []
|
||||
for m in rows:
|
||||
_nullify(m, ("round", "position"))
|
||||
m["session_ids"] = by_match.get((m["match_id"], m["type_bracket"]), [])
|
||||
out.append(m)
|
||||
return out
|
||||
|
||||
|
||||
@router.get("/api/tss/tournament/{tid}")
|
||||
async def tournament(tid: int) -> dict:
|
||||
meta = await db.query_one(db.tournaments_path(),
|
||||
"SELECT * FROM tournaments WHERE tournament_id = ?", (tid,))
|
||||
if not meta:
|
||||
raise HTTPException(status_code=404, detail=f"tournament {tid} not found")
|
||||
standings = await db.query(db.tournaments_path(),
|
||||
"SELECT * FROM tournament_standings WHERE tournament_id = ? ORDER BY group_index, rank",
|
||||
(tid,))
|
||||
return {"tournament": meta, "standings": standings, "matches": await _matches(tid)}
|
||||
|
||||
|
||||
@router.get("/api/tss/tournament/{tid}/standings")
|
||||
async def standings(tid: int) -> dict:
|
||||
rows = await db.query(db.tournaments_path(),
|
||||
"SELECT * FROM tournament_standings WHERE tournament_id = ? ORDER BY group_index, rank",
|
||||
(tid,))
|
||||
return {"tournament_id": tid, "standings": rows}
|
||||
|
||||
|
||||
@router.get("/api/tss/tournament/{tid}/matches")
|
||||
async def tournament_matches(tid: int) -> dict:
|
||||
rows = await _matches(tid)
|
||||
return {"tournament_id": tid, "total": len(rows), "matches": rows}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
_HERE = Path(__file__).resolve().parent.parent
|
||||
load_dotenv(_HERE / ".env")
|
||||
|
||||
import uvicorn # noqa: E402
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"web.api.app:app",
|
||||
host=os.getenv("TSS_API_HOST", "127.0.0.1"),
|
||||
port=int(os.getenv("TSS_API_PORT", "6100")),
|
||||
reload=False,
|
||||
)
|
||||
Reference in New Issue
Block a user