from __future__ import annotations import json import time from dataclasses import dataclass, field from pathlib import Path @dataclass class BotStateStore: base_dir: str = "./data/msteams/state" ttl_seconds: int = 7 * 24 * 3600 def __post_init__(self): Path(self.base_dir).mkdir(parents=True, exist_ok=True) def _file_path(self, namespace: str, key: str) -> Path: safe_key = key.replace("/", "_").replace(":", "_") return Path(self.base_dir) / f"{namespace}_{safe_key}.json" async def load(self, namespace: str, key: str) -> dict: path = self._file_path(namespace, key) if not path.exists(): return {} try: data = json.loads(path.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError): return {} if time.monotonic() - data.get("_updated_at", 0) > self.ttl_seconds: path.unlink(missing_ok=True) return {} return data.get("state", {}) async def save(self, namespace: str, key: str, state: dict) -> None: path = self._file_path(namespace, key) data = {"state": state, "_updated_at": time.monotonic()} path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") async def delete(self, namespace: str, key: str) -> None: path = self._file_path(namespace, key) path.unlink(missing_ok=True) async def get_user_state(self, user_id: str) -> dict: return await self.load("user", user_id) async def set_user_state(self, user_id: str, state: dict) -> None: await self.save("user", user_id, state) async def get_conversation_state(self, conversation_id: str) -> dict: return await self.load("conversation", conversation_id) async def set_conversation_state(self, conversation_id: str, state: dict) -> None: await self.save("conversation", conversation_id, state) async def get_private_state(self, user_id: str, conversation_id: str) -> dict: key = f"{user_id}_{conversation_id}" return await self.load("private", key) async def set_private_state(self, user_id: str, conversation_id: str, state: dict) -> None: key = f"{user_id}_{conversation_id}" await self.save("private", key, state)