Ctrl K
ComunidadGuías…

src/webui/services/bot_access.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Bot 渠道接入:游戏绑定凭证与代表玩家校验。"""

from __future__ import annotations

import secrets
from dataclasses import dataclass
from typing import Any, Callable, Protocol

from src.engine.modules import adventure_runtime_state, room_access
from src.gameplays import contracts as gameplay_contracts


GameKey = tuple[str, ...]


class BotAccessRegistry(Protocol):
    def get(self, game_key: GameKey) -> Any | None: ...
    async def save(self, instance: Any) -> None: ...


@dataclass(frozen=True)
class BotAccessDependencies:
    registry: BotAccessRegistry
    parse_game_key: Callable[[str], GameKey]


async def get_bind_token(dependencies: BotAccessDependencies, game_key: str, rotate: bool = False) -> dict[str, Any]:
    inst = dependencies.registry.get(dependencies.parse_game_key(game_key))
    if not inst:
        return {"ok": False, "error": "游戏不存在"}
    if rotate or not room_access.bot_bind_token(inst):
        inst.set_bot_bind_token(secrets.token_urlsafe(18))
        await dependencies.registry.save(inst)
    return {"ok": True, "bind_token": room_access.bot_bind_token(inst)}


async def verify_bind_game(dependencies: BotAccessDependencies, game_key: str, bind_token: str) -> dict[str, Any]:
    inst = dependencies.registry.get(dependencies.parse_game_key(game_key))
    if not inst:
        return {"ok": False, "error": "游戏不存在"}
    expected = str(room_access.bot_bind_token(inst) or "")
    if not expected or not secrets.compare_digest(expected, str(bind_token or "")):
        return {"ok": False, "error": "绑定凭证无效或已使用,请由 GM 在网页重新生成一次性绑定命令"}
    surface = adventure_runtime_state.effective_bot_surface(inst)
    if surface == gameplay_contracts.BOT_SURFACE_NONE:
        return {"ok": False, "error": "该玩法模式不支持群聊绑定"}
    result = {
        "ok": True,
        "game_key": game_key,
        "gm_uid": inst.gm_uid,
        "world_name": inst.world_name,
        "language": str(getattr(inst, "language", "") or "zh-CN"),
        "player_access_open": bool(room_access.player_access_open(inst)),
        "bot_surface": surface,
        "players": [
            {
                "user_id": user_id,
                "character_name": str(player.get("character_name") or user_id),
            }
            for user_id, player in inst.players.items()
        ],
    }
    inst.set_bot_bind_token("")
    await dependencies.registry.save(inst)
    return result


def actor_allowed(dependencies: BotAccessDependencies, game_key: str, user_id: str) -> bool:
    inst = dependencies.registry.get(dependencies.parse_game_key(game_key))
    return bool(inst and user_id and user_id in inst.players)


def surface_for_game(dependencies: BotAccessDependencies, game_key: str) -> str:
    """Return the authoritative Bot capability for a bound game.

    The access middleware calls this for every game-scoped Bot request.  Keep
    unknown or malformed state fail-closed instead of treating it as the
    historical full-access surface.
    """

    inst = dependencies.registry.get(dependencies.parse_game_key(game_key))
    if not inst:
        return gameplay_contracts.BOT_SURFACE_NONE
    return adventure_runtime_state.effective_bot_surface(inst)


class BotAccessService:
    """Bot binding and actor checks against one explicit registry boundary."""

    def __init__(self, dependencies: BotAccessDependencies) -> None:
        self._dependencies = dependencies

    async def get_bind_token(
        self, game_key: str, rotate: bool = False,
    ) -> dict[str, Any]:
        return await get_bind_token(self._dependencies, game_key, rotate)

    async def verify_bind_game(
        self, game_key: str, bind_token: str,
    ) -> dict[str, Any]:
        return await verify_bind_game(self._dependencies, game_key, bind_token)

    def actor_allowed(self, game_key: str, user_id: str) -> bool:
        return actor_allowed(self._dependencies, game_key, user_id)

    def surface_for_game(self, game_key: str) -> str:
        return surface_for_game(self._dependencies, game_key)