这是一个批量整理提交,包含以下主要改动: 1. 删除多处冗余的空行和未使用的导入 2. 修复文件末尾缺少换行符的问题 3. 调整部分模块的导入顺序与代码排版 4. 修复部分配置默认值与策略逻辑 5. 新增多个功能模块与辅助工具 6. 完善异常处理与日志记录 7. 修复速率限制、消息缓存、权限校验等逻辑bug 8. 废弃部分旧有API与配置项并添加警告提示
190 lines
5.9 KiB
Python
190 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, TYPE_CHECKING
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
if TYPE_CHECKING:
|
|
from .client import UrbitClient
|
|
|
|
|
|
_MIGRATABLE_KEYS = [
|
|
"dmAllowlist",
|
|
"groupChannels",
|
|
"autoDiscoverChannels",
|
|
"ownerShip",
|
|
"autoAcceptGroupInvites",
|
|
"autoAcceptDmInvites",
|
|
"groupInviteAllowlist",
|
|
]
|
|
|
|
|
|
class SettingsStore:
|
|
def __init__(self, client: UrbitClient, desk: str = "moltbot"):
|
|
self._client = client
|
|
self._desk = desk
|
|
self._store: dict[str, Any] = {}
|
|
self._file_config: dict[str, Any] = {}
|
|
self._loaded = False
|
|
self._version: int = 0
|
|
self._key_versions: dict[str, int] = {}
|
|
|
|
async def load(self, file_config: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
if file_config:
|
|
self._file_config = file_config
|
|
|
|
try:
|
|
from .scry import scry_settings
|
|
|
|
store_data = await scry_settings(self._client, self._desk)
|
|
if store_data and isinstance(store_data, dict):
|
|
self._store = store_data
|
|
self._version += 1
|
|
logger.info(f"[Urbit] Settings Store loaded from desk '{self._desk}' (v{self._version})")
|
|
else:
|
|
logger.debug(f"[Urbit] Settings Store empty or not found for desk '{self._desk}'")
|
|
except Exception as e:
|
|
logger.warning(f"[Urbit] Failed to load Settings Store: {e}")
|
|
|
|
self._loaded = True
|
|
return self._store
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
store_val = self._store.get(key)
|
|
if store_val is not None:
|
|
if isinstance(store_val, list) and len(store_val) == 0:
|
|
return []
|
|
return store_val
|
|
|
|
return self._file_config.get(key, default)
|
|
|
|
def get_all(self) -> dict[str, Any]:
|
|
merged = dict(self._file_config)
|
|
for key, value in self._store.items():
|
|
if value is not None:
|
|
merged[key] = value
|
|
return merged
|
|
|
|
def merge_key(self, key: str, value: Any, expected_version: int | None = None) -> bool:
|
|
if expected_version is not None:
|
|
current_version = self._key_versions.get(key, 0)
|
|
if expected_version < current_version:
|
|
logger.debug(
|
|
f"[Urbit] Settings merge rejected for '{key}': version {expected_version} < {current_version}"
|
|
)
|
|
return False
|
|
|
|
store = self._store.copy()
|
|
store[key] = value
|
|
self._store = store
|
|
self._key_versions[key] = self._key_versions.get(key, 0) + 1
|
|
return True
|
|
|
|
def replace_all(self, data: dict[str, Any]) -> None:
|
|
self._store = dict(data)
|
|
self._version += 1
|
|
logger.debug(f"[Urbit] Settings Store replaced (v{self._version})")
|
|
|
|
def update_key(self, key: str, value: Any) -> None:
|
|
store = dict(self._store)
|
|
store[key] = value
|
|
self._store = store
|
|
self._key_versions[key] = self._key_versions.get(key, 0) + 1
|
|
self._version += 1
|
|
|
|
async def reload(self) -> None:
|
|
self._version += 1
|
|
await self.load(self._file_config)
|
|
|
|
async def build_settings_migrations(self) -> list[dict[str, Any]]:
|
|
migrations: list[dict[str, Any]] = []
|
|
for key in _MIGRATABLE_KEYS:
|
|
if self.should_migrate_setting(key):
|
|
migrations.append(
|
|
{
|
|
"bucket_key": "moltbot",
|
|
"entry_key": key,
|
|
"value": self._file_config.get(key),
|
|
}
|
|
)
|
|
return migrations
|
|
|
|
def should_migrate_setting(self, key: str) -> bool:
|
|
file_val = self._file_config.get(key)
|
|
if file_val is None:
|
|
return False
|
|
store_val = self._store.get(key)
|
|
return store_val is None
|
|
|
|
async def poke_migration(self, bucket_key: str, entry_key: str, value: Any) -> bool:
|
|
import json as _json
|
|
|
|
try:
|
|
value_str = _json.dumps(value) if not isinstance(value, str) else value
|
|
payload = {
|
|
"action": "poke",
|
|
"ship": self._client.ship_name,
|
|
"app": "settings",
|
|
"mark": "settings-event",
|
|
"json": {
|
|
"put-entry": {
|
|
"bucket-key": bucket_key,
|
|
"entry-key": entry_key,
|
|
"value": value_str,
|
|
}
|
|
},
|
|
}
|
|
r = await self._client.put("/~/channel/settings-store", json=payload, timeout=10.0)
|
|
r.raise_for_status()
|
|
logger.info(f"[Urbit] Migrated setting: {entry_key} → Settings Store")
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f"[Urbit] Migration failed for {entry_key}: {e}")
|
|
return False
|
|
|
|
@property
|
|
def is_loaded(self) -> bool:
|
|
return self._loaded
|
|
|
|
@property
|
|
def version(self) -> int:
|
|
return self._version
|
|
|
|
|
|
def parse_channel_rules(raw: Any) -> dict[str, Any] | None:
|
|
if raw is None:
|
|
return None
|
|
if isinstance(raw, dict):
|
|
return raw
|
|
if isinstance(raw, str):
|
|
import json as _json
|
|
|
|
try:
|
|
return _json.loads(raw)
|
|
except _json.JSONDecodeError:
|
|
logger.warning("[Urbit] parse_channel_rules: invalid JSON string")
|
|
return None
|
|
return None
|
|
|
|
|
|
def parse_pending_approvals(raw: Any) -> list[dict[str, Any]] | None:
|
|
if raw is None:
|
|
return None
|
|
if isinstance(raw, list):
|
|
return raw
|
|
if isinstance(raw, str):
|
|
import json as _json
|
|
|
|
try:
|
|
return _json.loads(raw)
|
|
except _json.JSONDecodeError:
|
|
logger.warning("[Urbit] parse_pending_approvals: invalid JSON string")
|
|
return None
|
|
return None
|
|
|
|
|
|
def serialize_to_json_value(value: Any) -> str:
|
|
import json as _json
|
|
|
|
return _json.dumps(value)
|