TSS storage (#1233)
This commit is contained in:
+236
@@ -0,0 +1,236 @@
|
||||
"""TSSBOT storage layer — SQLite paths + idempotent DB init.
|
||||
|
||||
Two databases live under ``STORAGE_VOL_PATH`` (set in ``TSSBOT/.env``):
|
||||
|
||||
* ``tss_teams.db`` — persistent team registry (analogue of SREBOT's squadrons.db)
|
||||
* ``tss_battles.db`` — per-match summary + per-player game history
|
||||
(analogue of SREBOT's sq_battles.db)
|
||||
|
||||
Schemas mirror SREBOT shape so query patterns transfer directly. The
|
||||
TSS-specific differences are:
|
||||
|
||||
* "squadron" → "team" / "clan_id" → "team_id"
|
||||
* Spectra ships an untransformed per-player stat blob, so we keep extra
|
||||
columns the raw payload provides (``score``, ``missile_evades``,
|
||||
``shell_interceptions``, ``team_kills_stat``, ``country_id``, ``team_slot``).
|
||||
* No vehicle-translate / stat-divide transform — one row per
|
||||
``(UID, session_id, vehicle_internal)`` for each used unit, with the
|
||||
player's full stat line copied onto each row. Aggregation queries that
|
||||
sum across vehicles must ``DISTINCT`` on session_id first.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import aiosqlite
|
||||
|
||||
log = logging.getLogger("tssbot.storage")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Paths
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _require_storage_dir() -> Path:
|
||||
raw = os.environ.get("STORAGE_VOL_PATH", "").strip()
|
||||
if not raw:
|
||||
raise RuntimeError("STORAGE_VOL_PATH must be set in TSSBOT/.env")
|
||||
p = Path(raw).expanduser().resolve()
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
|
||||
STORAGE_DIR: Path = _require_storage_dir()
|
||||
TSS_BATTLES_DB_PATH: Path = STORAGE_DIR / "tss_battles.db"
|
||||
TSS_TEAMS_DB_PATH: Path = STORAGE_DIR / "tss_teams.db"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# tss_battles.db — match_summary + player_games_hist
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_MATCH_SUMMARY_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS match_summary (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
map_name TEXT,
|
||||
mission_mode TEXT,
|
||||
starttime_unix INTEGER,
|
||||
endtime_unix INTEGER,
|
||||
draw INTEGER NOT NULL DEFAULT 0,
|
||||
winning_slot TEXT,
|
||||
losing_slot TEXT,
|
||||
winning_team TEXT,
|
||||
losing_team TEXT,
|
||||
winning_team_json TEXT,
|
||||
losing_team_json TEXT,
|
||||
received_unix INTEGER,
|
||||
winning_team_id INTEGER,
|
||||
losing_team_id INTEGER
|
||||
)
|
||||
"""
|
||||
|
||||
_MATCH_SUMMARY_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_map_name ON match_summary(map_name)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_endtime ON match_summary(endtime_unix)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_winning_team ON match_summary(winning_team)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_losing_team ON match_summary(losing_team)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_winning_team_id ON match_summary(winning_team_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_losing_team_id ON match_summary(losing_team_id)",
|
||||
]
|
||||
|
||||
|
||||
_PLAYER_GAMES_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS player_games_hist (
|
||||
UID TEXT NOT NULL,
|
||||
nick TEXT NOT NULL,
|
||||
team_name TEXT NOT NULL DEFAULT 'UNKNOWN',
|
||||
team_tag TEXT NOT NULL DEFAULT 'UNKNOWN',
|
||||
team_slot TEXT,
|
||||
session_id TEXT NOT NULL,
|
||||
vehicle TEXT,
|
||||
vehicle_internal TEXT,
|
||||
ground_kills INTEGER NOT NULL DEFAULT 0,
|
||||
air_kills INTEGER NOT NULL DEFAULT 0,
|
||||
assists INTEGER NOT NULL DEFAULT 0,
|
||||
captures INTEGER NOT NULL DEFAULT 0,
|
||||
deaths INTEGER NOT NULL DEFAULT 0,
|
||||
score INTEGER NOT NULL DEFAULT 0,
|
||||
missile_evades INTEGER NOT NULL DEFAULT 0,
|
||||
shell_interceptions INTEGER NOT NULL DEFAULT 0,
|
||||
team_kills_stat INTEGER NOT NULL DEFAULT 0,
|
||||
country_id INTEGER,
|
||||
victor_bool TEXT NOT NULL DEFAULT 'Loss',
|
||||
endtime_unix INTEGER NOT NULL DEFAULT 0,
|
||||
team_id INTEGER,
|
||||
UNIQUE (UID, session_id, vehicle_internal)
|
||||
)
|
||||
"""
|
||||
|
||||
_PLAYER_GAMES_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_pgh_session ON player_games_hist(session_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_pgh_uid_time ON player_games_hist(UID, endtime_unix)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_pgh_team_tag_time ON player_games_hist(team_tag, endtime_unix)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_pgh_team_name_time ON player_games_hist(team_name, endtime_unix)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_pgh_endtime ON player_games_hist(endtime_unix)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_pgh_nick ON player_games_hist(nick COLLATE NOCASE)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_pgh_session_team_name ON player_games_hist(session_id, team_name)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_pgh_team_id_time ON player_games_hist(team_id, endtime_unix)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_pgh_vehicle_internal ON player_games_hist(vehicle_internal)",
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# tss_teams.db — teams_data + team_members + team_name_history
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_TEAMS_DATA_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS teams_data (
|
||||
team_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
long_name TEXT NOT NULL UNIQUE,
|
||||
short_name TEXT UNIQUE,
|
||||
tag_name TEXT,
|
||||
description TEXT,
|
||||
region TEXT,
|
||||
members INTEGER NOT NULL DEFAULT 0,
|
||||
members_json TEXT,
|
||||
captain_uid TEXT,
|
||||
guild_id TEXT,
|
||||
created_unix INTEGER,
|
||||
updated_unix INTEGER
|
||||
)
|
||||
"""
|
||||
|
||||
_TEAMS_DATA_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_teams_data_tag_name ON teams_data(tag_name COLLATE NOCASE)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_teams_data_guild_id ON teams_data(guild_id)",
|
||||
]
|
||||
|
||||
|
||||
_TEAM_MEMBERS_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS team_members (
|
||||
team_id INTEGER NOT NULL,
|
||||
uid TEXT NOT NULL,
|
||||
nick TEXT,
|
||||
role TEXT NOT NULL DEFAULT 'player',
|
||||
joined_unix INTEGER,
|
||||
PRIMARY KEY (team_id, uid)
|
||||
)
|
||||
"""
|
||||
|
||||
_TEAM_MEMBERS_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_team_members_team_id ON team_members(team_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_team_members_uid ON team_members(uid)",
|
||||
]
|
||||
|
||||
|
||||
_TEAM_NAME_HISTORY_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS team_name_history (
|
||||
team_id INTEGER NOT NULL,
|
||||
long_name TEXT NOT NULL,
|
||||
first_seen INTEGER,
|
||||
last_seen INTEGER,
|
||||
PRIMARY KEY (team_id, long_name)
|
||||
)
|
||||
"""
|
||||
|
||||
_TEAM_NAME_HISTORY_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_team_name_history_name ON team_name_history(long_name COLLATE NOCASE)",
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Init
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_PRAGMAS = (
|
||||
"PRAGMA journal_mode=WAL;",
|
||||
"PRAGMA synchronous=NORMAL;",
|
||||
"PRAGMA busy_timeout=5000;",
|
||||
"PRAGMA temp_store=MEMORY;",
|
||||
)
|
||||
|
||||
|
||||
async def _apply(conn: aiosqlite.Connection, statements: list[str]) -> None:
|
||||
for sql in statements:
|
||||
await conn.execute(sql)
|
||||
|
||||
|
||||
async def _init_battles_db() -> None:
|
||||
async with aiosqlite.connect(TSS_BATTLES_DB_PATH) as conn:
|
||||
for sql in _PRAGMAS:
|
||||
await conn.execute(sql)
|
||||
await conn.execute(_MATCH_SUMMARY_SQL)
|
||||
await conn.execute(_PLAYER_GAMES_SQL)
|
||||
await _apply(conn, _MATCH_SUMMARY_INDEXES)
|
||||
await _apply(conn, _PLAYER_GAMES_INDEXES)
|
||||
await conn.commit()
|
||||
|
||||
|
||||
async def _init_teams_db() -> None:
|
||||
async with aiosqlite.connect(TSS_TEAMS_DB_PATH) as conn:
|
||||
for sql in _PRAGMAS:
|
||||
await conn.execute(sql)
|
||||
await conn.execute(_TEAMS_DATA_SQL)
|
||||
await conn.execute(_TEAM_MEMBERS_SQL)
|
||||
await conn.execute(_TEAM_NAME_HISTORY_SQL)
|
||||
await _apply(conn, _TEAMS_DATA_INDEXES)
|
||||
await _apply(conn, _TEAM_MEMBERS_INDEXES)
|
||||
await _apply(conn, _TEAM_NAME_HISTORY_INDEXES)
|
||||
await conn.commit()
|
||||
|
||||
|
||||
async def init_tss_dbs() -> None:
|
||||
"""Create both TSSBOT SQLite databases if they don't already exist.
|
||||
|
||||
Idempotent — safe to call on every startup. Sets WAL pragmas, creates
|
||||
tables, and ensures indexes.
|
||||
"""
|
||||
await _init_battles_db()
|
||||
await _init_teams_db()
|
||||
log.info(
|
||||
"TSS DBs initialised: %s, %s",
|
||||
TSS_BATTLES_DB_PATH,
|
||||
TSS_TEAMS_DB_PATH,
|
||||
)
|
||||
@@ -1,3 +1,6 @@
|
||||
# TSSBOT runtime deps — populate as the bot is built out.
|
||||
# Many shared utilities under BOTS/SHARED/ require the same deps SREBOT uses;
|
||||
# see ../SREBOT/requirements.txt for reference.
|
||||
aiosqlite
|
||||
discord.py
|
||||
python-dotenv
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Entry point for TSSBOT. Connects to Discord and idles until commands land."""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
@@ -17,6 +18,9 @@ from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(dotenv_path=_HERE / ".env")
|
||||
|
||||
# Imported after load_dotenv so STORAGE_VOL_PATH is read from .env.
|
||||
from BOT.storage import init_tss_dbs # noqa: E402
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="[%(asctime)s] [%(levelname)s] [tssbot] %(message)s",
|
||||
@@ -54,4 +58,9 @@ if __name__ == "__main__":
|
||||
if not TOKEN:
|
||||
log.error("DISCORD_KEY not set in TSSBOT/.env — aborting")
|
||||
sys.exit(1)
|
||||
try:
|
||||
asyncio.run(init_tss_dbs())
|
||||
except Exception as e:
|
||||
log.error(f"failed to initialise TSS databases: {e}")
|
||||
sys.exit(1)
|
||||
bot.run(TOKEN, log_handler=None)
|
||||
|
||||
Reference in New Issue
Block a user