mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Enhance Roblox API integration and refactor CLI commands
- Updated the `clap` dependency in `Cargo.toml` to include the "env" feature for improved command-line argument handling. - Introduced a new `roblox_api` module to encapsulate Roblox API interactions, including authentication and asset downloading. - Refactored the `CloneCommand` and `StudioCommand` to utilize the new `roblox_api` functions for fetching experience names and universe IDs, enhancing code clarity and reducing duplication. - Removed obsolete functions related to Roblox API calls from the CLI commands, streamlining the codebase. - Added error handling for authentication failures, ensuring better user feedback when required credentials are missing. These changes aim to improve the overall structure and functionality of the CLI while enhancing the integration with the Roblox API.
This commit is contained in:
parent
fb46515913
commit
9934094dbc
22 changed files with 2427 additions and 161 deletions
280
tools/atlas-daemon/atlas_daemon/db.py
Normal file
280
tools/atlas-daemon/atlas_daemon/db.py
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
GLOBAL_ADMIN_IDS = ["176643682689089545", "1010301288946339920"]
|
||||
|
||||
SCHEMA = """\
|
||||
CREATE TABLE IF NOT EXISTS allowed_servers (
|
||||
discord_server_id TEXT PRIMARY KEY,
|
||||
server_name TEXT,
|
||||
approved_by TEXT NOT NULL,
|
||||
approved_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
discord_user_id TEXT PRIMARY KEY,
|
||||
added_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS games (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
discord_server_id TEXT NOT NULL REFERENCES allowed_servers(discord_server_id) ON DELETE CASCADE,
|
||||
place_id INTEGER NOT NULL,
|
||||
channel_id TEXT NOT NULL,
|
||||
api_key_encrypted BLOB NOT NULL,
|
||||
added_by TEXT NOT NULL,
|
||||
working_dir TEXT NOT NULL,
|
||||
last_sync_at TEXT,
|
||||
last_error TEXT,
|
||||
last_error_at TEXT,
|
||||
error_count INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(discord_server_id, place_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS issues (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
game_id INTEGER NOT NULL REFERENCES games(id) ON DELETE CASCADE,
|
||||
discord_message_id TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
line_start INTEGER,
|
||||
line_end INTEGER,
|
||||
severity TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
explanation TEXT,
|
||||
suggestion TEXT,
|
||||
resolved INTEGER DEFAULT 0,
|
||||
resolved_by TEXT,
|
||||
resolved_reason TEXT,
|
||||
resolved_at TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_issues_game_unresolved ON issues(game_id, resolved);
|
||||
CREATE INDEX IF NOT EXISTS idx_games_server ON games(discord_server_id);
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Game:
|
||||
id: int
|
||||
discord_server_id: str
|
||||
place_id: int
|
||||
channel_id: str
|
||||
api_key_encrypted: bytes
|
||||
added_by: str
|
||||
working_dir: str
|
||||
last_sync_at: str | None
|
||||
last_error: str | None
|
||||
last_error_at: str | None
|
||||
error_count: int
|
||||
created_at: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class DBIssue:
|
||||
id: int
|
||||
game_id: int
|
||||
discord_message_id: str | None
|
||||
file_path: str
|
||||
line_start: int | None
|
||||
line_end: int | None
|
||||
severity: str
|
||||
title: str
|
||||
explanation: str | None
|
||||
suggestion: str | None
|
||||
resolved: bool
|
||||
resolved_by: str | None
|
||||
resolved_reason: str | None
|
||||
resolved_at: str | None
|
||||
created_at: str
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, db_path: Path) -> None:
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._conn = sqlite3.connect(str(db_path), check_same_thread=False)
|
||||
self._conn.execute("PRAGMA journal_mode=WAL")
|
||||
self._conn.execute("PRAGMA foreign_keys=ON")
|
||||
self._conn.executescript(SCHEMA)
|
||||
self._seed_admins()
|
||||
|
||||
def _seed_admins(self) -> None:
|
||||
for uid in GLOBAL_ADMIN_IDS:
|
||||
self._conn.execute(
|
||||
"INSERT OR IGNORE INTO admins (discord_user_id) VALUES (?)", (uid,)
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def is_admin(self, user_id: str) -> bool:
|
||||
row = self._conn.execute(
|
||||
"SELECT 1 FROM admins WHERE discord_user_id = ?", (user_id,)
|
||||
).fetchone()
|
||||
return row is not None
|
||||
|
||||
# -- Server whitelist --
|
||||
|
||||
def is_server_allowed(self, server_id: str) -> bool:
|
||||
row = self._conn.execute(
|
||||
"SELECT 1 FROM allowed_servers WHERE discord_server_id = ?", (server_id,)
|
||||
).fetchone()
|
||||
return row is not None
|
||||
|
||||
def approve_server(
|
||||
self, server_id: str, server_name: str | None, approved_by: str
|
||||
) -> None:
|
||||
self._conn.execute(
|
||||
"INSERT OR REPLACE INTO allowed_servers (discord_server_id, server_name, approved_by) VALUES (?, ?, ?)",
|
||||
(server_id, server_name, approved_by),
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def revoke_server(self, server_id: str) -> None:
|
||||
self._conn.execute(
|
||||
"DELETE FROM allowed_servers WHERE discord_server_id = ?", (server_id,)
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
# -- Games --
|
||||
|
||||
def add_game(
|
||||
self,
|
||||
server_id: str,
|
||||
place_id: int,
|
||||
channel_id: str,
|
||||
api_key_encrypted: bytes,
|
||||
added_by: str,
|
||||
working_dir: str,
|
||||
) -> int:
|
||||
cur = self._conn.execute(
|
||||
"INSERT INTO games (discord_server_id, place_id, channel_id, api_key_encrypted, added_by, working_dir) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(server_id, place_id, channel_id, api_key_encrypted, added_by, working_dir),
|
||||
)
|
||||
self._conn.commit()
|
||||
return cur.lastrowid # type: ignore[return-value]
|
||||
|
||||
def remove_game(self, server_id: str, place_id: int) -> bool:
|
||||
cur = self._conn.execute(
|
||||
"DELETE FROM games WHERE discord_server_id = ? AND place_id = ?",
|
||||
(server_id, place_id),
|
||||
)
|
||||
self._conn.commit()
|
||||
return cur.rowcount > 0
|
||||
|
||||
def get_game(self, server_id: str, place_id: int) -> Game | None:
|
||||
row = self._conn.execute(
|
||||
"SELECT * FROM games WHERE discord_server_id = ? AND place_id = ?",
|
||||
(server_id, place_id),
|
||||
).fetchone()
|
||||
return Game(*row) if row else None
|
||||
|
||||
def get_games_for_server(self, server_id: str) -> list[Game]:
|
||||
rows = self._conn.execute(
|
||||
"SELECT * FROM games WHERE discord_server_id = ?", (server_id,)
|
||||
).fetchall()
|
||||
return [Game(*r) for r in rows]
|
||||
|
||||
def get_all_games(self) -> list[Game]:
|
||||
rows = self._conn.execute("SELECT * FROM games").fetchall()
|
||||
return [Game(*r) for r in rows]
|
||||
|
||||
def count_games_for_server(self, server_id: str) -> int:
|
||||
row = self._conn.execute(
|
||||
"SELECT COUNT(*) FROM games WHERE discord_server_id = ?", (server_id,)
|
||||
).fetchone()
|
||||
return row[0] if row else 0
|
||||
|
||||
def update_last_sync(self, game_id: int) -> None:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
self._conn.execute(
|
||||
"UPDATE games SET last_sync_at = ?, last_error = NULL, last_error_at = NULL, error_count = 0 WHERE id = ?",
|
||||
(now, game_id),
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def update_last_error(self, game_id: int, error: str) -> bool:
|
||||
"""Update error state. Returns True if this is a new error or throttle window expired."""
|
||||
row = self._conn.execute(
|
||||
"SELECT last_error, error_count FROM games WHERE id = ?", (game_id,)
|
||||
).fetchone()
|
||||
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
if row and row[0] == error:
|
||||
new_count = (row[1] or 0) + 1
|
||||
self._conn.execute(
|
||||
"UPDATE games SET error_count = ? WHERE id = ?",
|
||||
(new_count, game_id),
|
||||
)
|
||||
self._conn.commit()
|
||||
return new_count == 1 or new_count % 12 == 0
|
||||
else:
|
||||
self._conn.execute(
|
||||
"UPDATE games SET last_error = ?, last_error_at = ?, error_count = 1 WHERE id = ?",
|
||||
(error, now, game_id),
|
||||
)
|
||||
self._conn.commit()
|
||||
return True
|
||||
|
||||
# -- Issues --
|
||||
|
||||
def get_unresolved_issues(self, game_id: int) -> list[DBIssue]:
|
||||
rows = self._conn.execute(
|
||||
"SELECT * FROM issues WHERE game_id = ? AND resolved = 0", (game_id,)
|
||||
).fetchall()
|
||||
return [DBIssue(*r) for r in rows]
|
||||
|
||||
def add_issue(
|
||||
self,
|
||||
game_id: int,
|
||||
discord_message_id: str | None,
|
||||
file_path: str,
|
||||
line_start: int | None,
|
||||
line_end: int | None,
|
||||
severity: str,
|
||||
title: str,
|
||||
explanation: str | None,
|
||||
suggestion: str | None,
|
||||
) -> int:
|
||||
cur = self._conn.execute(
|
||||
"INSERT INTO issues (game_id, discord_message_id, file_path, line_start, line_end, "
|
||||
"severity, title, explanation, suggestion) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(game_id, discord_message_id, file_path, line_start, line_end, severity, title, explanation, suggestion),
|
||||
)
|
||||
self._conn.commit()
|
||||
return cur.lastrowid # type: ignore[return-value]
|
||||
|
||||
def resolve_issue(
|
||||
self, issue_id: int, resolved_by: str, reason: str = "manual"
|
||||
) -> None:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
self._conn.execute(
|
||||
"UPDATE issues SET resolved = 1, resolved_by = ?, resolved_reason = ?, resolved_at = ? WHERE id = ?",
|
||||
(resolved_by, reason, now, issue_id),
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def update_issue_message_id(self, issue_id: int, message_id: str) -> None:
|
||||
self._conn.execute(
|
||||
"UPDATE issues SET discord_message_id = ? WHERE id = ?",
|
||||
(message_id, issue_id),
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def get_issue_by_message_id(self, message_id: str) -> DBIssue | None:
|
||||
row = self._conn.execute(
|
||||
"SELECT * FROM issues WHERE discord_message_id = ?", (message_id,)
|
||||
).fetchone()
|
||||
return DBIssue(*row) if row else None
|
||||
|
||||
def final_dedup(self, game_id: int, file_path: str, title: str) -> bool:
|
||||
"""Returns True if this (file, title) pair already exists as unresolved."""
|
||||
row = self._conn.execute(
|
||||
"SELECT 1 FROM issues WHERE game_id = ? AND file_path = ? AND title = ? AND resolved = 0",
|
||||
(game_id, file_path, title),
|
||||
).fetchone()
|
||||
return row is not None
|
||||
Loading…
Add table
Add a link
Reference in a new issue