24335a2677
* feat(gateway): hashed key store with grant + hot reload Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(gateway): channel registry + aiohttp app (keyed auth, whoami, per-channel ws/proxy) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(gateway): manage_keys CLI (add/list/revoke) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(gateway): retire srebot_external, run relay-gateway under PM2 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(gateway): point ecosystem + README at relay-gateway Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss): replay outbox producer for relay gateway Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss): forward processed games to relay outbox Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss-api): db helpers, app skeleton, info endpoint, fixtures Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss-api): player, games, history, search endpoints Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss-api): live, match, scoreboard, matches-search, maps Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss-api): filter-required leaderboards (players/vehicles/stats) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss-api): tournament list/detail/standings/matches Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: wire tss upstream through gateway + tssbot-api PM2 app Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
3.7 KiB
Python
78 lines
3.7 KiB
Python
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}
|