OpenSpace/openspace/protocol/slash_commands.py
2026-07-17 11:43:42 +08:00

106 lines
4.1 KiB
Python

from __future__ import annotations
import json
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Any
_SCHEMA_PATH = Path(__file__).with_name("schema") / "slash_commands.json"
@dataclass(frozen=True, slots=True)
class SlashCommandArg:
name: str
required: bool
description: str
@dataclass(frozen=True, slots=True)
class CoreSlashCommand:
name: str
summary: str
usage: str
category: str
aliases: tuple[str, ...] = ()
args: tuple[SlashCommandArg, ...] = ()
tui_visible: bool = True
@lru_cache(maxsize=1)
def load_slash_command_manifest() -> dict[str, Any]:
manifest = json.loads(_SCHEMA_PATH.read_text(encoding="utf-8"))
if manifest.get("manifest_schema_version") != 1:
raise ValueError("slash_commands.json manifest_schema_version must be 1")
commands = manifest.get("commands")
if not isinstance(commands, list):
raise ValueError("slash_commands.json commands must be a list")
names: set[str] = set()
for raw in commands:
if not isinstance(raw, dict):
raise ValueError("slash command entries must be objects")
name = raw.get("name")
if not isinstance(name, str) or not name:
raise ValueError("slash command entries must include a non-empty name")
if name in names:
raise ValueError(f"duplicate slash command {name!r}")
names.add(name)
if raw.get("handler") not in {"local", "core"}:
raise ValueError(f"slash command {name!r} handler must be local or core")
if not isinstance(raw.get("category"), str) or not raw["category"]:
raise ValueError(f"slash command {name!r} category must be a non-empty string")
aliases = raw.get("aliases", [])
if not isinstance(aliases, list) or not all(isinstance(alias, str) for alias in aliases):
raise ValueError(f"slash command {name!r} aliases must be a list of strings")
args = raw.get("args", [])
if not isinstance(args, list):
raise ValueError(f"slash command {name!r} args must be a list")
for arg in args:
if not isinstance(arg, dict):
raise ValueError(f"slash command {name!r} args must be objects")
if not isinstance(arg.get("name"), str) or not arg["name"]:
raise ValueError(f"slash command {name!r} arg names must be non-empty strings")
if not isinstance(arg.get("required"), bool):
raise ValueError(f"slash command {name!r} arg required must be boolean")
if not isinstance(arg.get("description"), str):
raise ValueError(f"slash command {name!r} arg description must be string")
return manifest
def get_slash_command_categories() -> dict[str, str]:
categories = load_slash_command_manifest().get("categories", {})
if not isinstance(categories, dict):
raise ValueError("slash_commands.json categories must be an object")
return {str(name): str(label) for name, label in categories.items()}
def get_slash_commands() -> tuple[dict[str, Any], ...]:
return tuple(dict(command) for command in load_slash_command_manifest()["commands"])
def get_core_slash_commands() -> tuple[CoreSlashCommand, ...]:
parsed: list[CoreSlashCommand] = []
for raw in load_slash_command_manifest()["commands"]:
if raw["handler"] != "core":
continue
aliases = raw.get("aliases", [])
args = raw.get("args", [])
parsed.append(
CoreSlashCommand(
name=str(raw["name"]),
summary=str(raw["summary"]),
usage=str(raw["usage"]),
category=str(raw["category"]),
aliases=tuple(str(alias) for alias in aliases),
args=tuple(
SlashCommandArg(
name=str(arg["name"]),
required=bool(arg["required"]),
description=str(arg["description"]),
)
for arg in args
),
tui_visible=bool(raw.get("tui_visible", True)),
)
)
return tuple(parsed)