这是一个批量整理提交,包含以下主要改动: 1. 删除多处冗余的空行和未使用的导入 2. 修复文件末尾缺少换行符的问题 3. 调整部分模块的导入顺序与代码排版 4. 修复部分配置默认值与策略逻辑 5. 新增多个功能模块与辅助工具 6. 完善异常处理与日志记录 7. 修复速率限制、消息缓存、权限校验等逻辑bug 8. 废弃部分旧有API与配置项并添加警告提示
270 lines
9.9 KiB
Python
270 lines
9.9 KiB
Python
from __future__ import annotations
|
|
|
|
import time
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
|
|
@dataclass
|
|
class PendingApproval:
|
|
id: str
|
|
approval_type: str
|
|
requester_ship: str
|
|
channel_id: str
|
|
message_content: str
|
|
created_at: float
|
|
status: str = "pending"
|
|
|
|
def approve(self) -> None:
|
|
self.status = "approved"
|
|
|
|
def deny(self) -> None:
|
|
self.status = "denied"
|
|
|
|
|
|
class ApprovalSystem:
|
|
def __init__(self, owner_ship: str = ""):
|
|
self._owner_ship = owner_ship.lstrip("~")
|
|
self._pending: dict[str, PendingApproval] = {}
|
|
self._resolved: dict[str, PendingApproval] = {}
|
|
self._blocked_ships: set[str] = set()
|
|
|
|
@property
|
|
def owner_ship(self) -> str:
|
|
return self._owner_ship
|
|
|
|
def create_pending(
|
|
self,
|
|
approval_type: str,
|
|
requester_ship: str,
|
|
channel_id: str,
|
|
message_content: str,
|
|
) -> PendingApproval:
|
|
approval = PendingApproval(
|
|
id=f"{approval_type}-{int(time.time())}-{str(uuid.uuid4())[:8]}",
|
|
approval_type=approval_type,
|
|
requester_ship=requester_ship,
|
|
channel_id=channel_id,
|
|
message_content=message_content,
|
|
created_at=time.time(),
|
|
)
|
|
self._pending[approval.id] = approval
|
|
logger.info(f"[Urbit] Approval {approval.id} created: {approval_type} from ~{requester_ship}")
|
|
return approval
|
|
|
|
def approve(self, approval_id: str) -> PendingApproval | None:
|
|
approval = self._pending.pop(approval_id, None)
|
|
if approval:
|
|
approval.approve()
|
|
self._resolved[approval_id] = approval
|
|
logger.info(f"[Urbit] Approval {approval_id} approved")
|
|
return approval
|
|
|
|
def deny(self, approval_id: str) -> PendingApproval | None:
|
|
approval = self._pending.pop(approval_id, None)
|
|
if approval:
|
|
approval.deny()
|
|
self._resolved[approval_id] = approval
|
|
logger.info(f"[Urbit] Approval {approval_id} denied")
|
|
return approval
|
|
|
|
def get_pending(self) -> list[PendingApproval]:
|
|
return list(self._pending.values())
|
|
|
|
def get_pending_count(self) -> int:
|
|
return len(self._pending)
|
|
|
|
def block_ship(self, ship: str) -> bool:
|
|
ship = ship.lstrip("~").lower()
|
|
if ship in self._blocked_ships:
|
|
return False
|
|
self._blocked_ships.add(ship)
|
|
logger.info(f"[Urbit] Ship blocked: ~{ship}")
|
|
return True
|
|
|
|
def unblock_ship(self, ship: str) -> bool:
|
|
ship = ship.lstrip("~").lower()
|
|
if ship not in self._blocked_ships:
|
|
return False
|
|
self._blocked_ships.discard(ship)
|
|
logger.info(f"[Urbit] Ship unblocked: ~{ship}")
|
|
return True
|
|
|
|
def get_blocked_ships(self) -> list[str]:
|
|
return sorted(self._blocked_ships)
|
|
|
|
def is_ship_blocked(self, ship: str) -> bool:
|
|
return ship.lstrip("~").lower() in self._blocked_ships
|
|
|
|
def has_approved(self, ship: str) -> bool:
|
|
normalized = ship.lstrip("~").lower()
|
|
for approval in self._pending.values():
|
|
if approval.requester_ship.lstrip("~").lower() == normalized and approval.status == "approved":
|
|
return True
|
|
for approval in self._resolved.values():
|
|
if approval.requester_ship.lstrip("~").lower() == normalized and approval.status == "approved":
|
|
return True
|
|
return False
|
|
|
|
def has_duplicate_pending(self, approval_type: str, requester_ship: str, channel_id: str) -> bool:
|
|
req_ship = requester_ship.lstrip("~").lower()
|
|
for p in self._pending.values():
|
|
if (
|
|
p.approval_type == approval_type
|
|
and p.requester_ship.lstrip("~").lower() == req_ship
|
|
and p.channel_id == channel_id
|
|
and p.status == "pending"
|
|
):
|
|
return True
|
|
return False
|
|
|
|
def build_owner_notification(self, approval: PendingApproval) -> str:
|
|
return (
|
|
f"New {approval.approval_type} request from ~{approval.requester_ship}"
|
|
f" (ID: {approval.id}): {approval.message_content[:200]}"
|
|
)
|
|
|
|
def build_approval_instructions(self, approval_id: str) -> str:
|
|
return (
|
|
f"To approve: send `approve {approval_id}`\n"
|
|
f"To deny: send `deny {approval_id}`\n"
|
|
f"To block: send `block {approval_id}`\n"
|
|
f"To list pending: send `pending`\n"
|
|
f"To list blocked: send `blocked`"
|
|
)
|
|
|
|
def is_approval_command(self, content: str) -> bool:
|
|
return any(
|
|
content.strip().startswith(cmd)
|
|
for cmd in ("approve ", "deny ", "block ", "unblock ", "approvals", "pending", "blocked")
|
|
)
|
|
|
|
def handle_admin_command(self, content: str, sender_ship: str) -> str | None:
|
|
if not self._owner_ship or sender_ship.lower() != self._owner_ship.lower():
|
|
return None
|
|
|
|
content = content.strip()
|
|
|
|
if content.startswith("approve "):
|
|
aid = content.split(" ", 1)[1].strip()
|
|
approval = self.approve(aid)
|
|
if approval:
|
|
return f"Approved {aid} (~{approval.requester_ship})"
|
|
return f"Approval {aid} not found"
|
|
|
|
if content.startswith("deny "):
|
|
aid = content.split(" ", 1)[1].strip()
|
|
approval = self.deny(aid)
|
|
if approval:
|
|
return f"Denied {aid} (~{approval.requester_ship})"
|
|
return f"Approval {aid} not found"
|
|
|
|
if content.startswith("block "):
|
|
aid = content.split(" ", 1)[1].strip()
|
|
approval = self._pending.get(aid)
|
|
if approval:
|
|
self.block_ship(approval.requester_ship)
|
|
self.deny(aid)
|
|
return f"Blocked and denied {aid} (~{approval.requester_ship})"
|
|
return f"Approval {aid} not found"
|
|
|
|
if content.startswith("unblock "):
|
|
ship = content.split(" ", 1)[1].strip()
|
|
if self.unblock_ship(ship):
|
|
return f"Unblocked ~{ship}"
|
|
return f"Ship ~{ship} is not blocked"
|
|
|
|
if content in ("approvals", "pending"):
|
|
pending = self.get_pending()
|
|
if not pending:
|
|
return "No pending approvals"
|
|
lines = ["Pending approvals:"]
|
|
for p in pending:
|
|
lines.append(f" {p.id}: {p.approval_type} from ~{p.requester_ship}")
|
|
return "\n".join(lines)
|
|
|
|
if content == "blocked":
|
|
blocked = self.get_blocked_ships()
|
|
if not blocked:
|
|
return "No blocked ships"
|
|
lines = ["Blocked ships:"]
|
|
for ship in blocked:
|
|
lines.append(f" ~{ship}")
|
|
return "\n".join(lines)
|
|
|
|
return None
|
|
|
|
async def persist_to_settings_store(self, store: SettingsStore) -> None:
|
|
from .settings import SettingsStore
|
|
|
|
store_data: dict[str, Any] = {
|
|
"pending_approvals": [],
|
|
"blocked_ships": sorted(self._blocked_ships),
|
|
}
|
|
for p in self._pending.values():
|
|
store_data["pending_approvals"].append(
|
|
{
|
|
"id": p.id,
|
|
"type": p.approval_type,
|
|
"requester": p.requester_ship,
|
|
"channel": p.channel_id,
|
|
"content": p.message_content[:500],
|
|
"created_at": p.created_at,
|
|
"status": p.status,
|
|
}
|
|
)
|
|
try:
|
|
ok_pending = store.merge_key("pendingApprovals", store_data["pending_approvals"])
|
|
ok_blocked = store.merge_key("blockedShips", store_data["blocked_ships"])
|
|
if not ok_pending or not ok_blocked:
|
|
logger.warning("[Urbit] Settings Store merge conflict, retrying...")
|
|
await store.reload()
|
|
store.merge_key("pendingApprovals", store_data["pending_approvals"])
|
|
store.merge_key("blockedShips", store_data["blocked_ships"])
|
|
logger.debug(
|
|
f"[Urbit] Persisted {len(self._pending)} pending approvals "
|
|
f"and {len(self._blocked_ships)} blocked ships to Settings Store"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"[Urbit] Failed to persist approvals: {e}")
|
|
|
|
async def load_from_settings_store(self, client: Any) -> list[PendingApproval]:
|
|
from .settings import SettingsStore
|
|
|
|
try:
|
|
store = SettingsStore(client)
|
|
data = await store.load()
|
|
stored = data.get("pending_approvals", [])
|
|
stored_blocked = data.get("blocked_ships", [])
|
|
if isinstance(stored_blocked, list):
|
|
for ship in stored_blocked:
|
|
self._blocked_ships.add(ship.lstrip("~").lower())
|
|
loaded: list[PendingApproval] = []
|
|
for item in stored:
|
|
approval = PendingApproval(
|
|
id=item.get("id", str(uuid.uuid4())[:8]),
|
|
approval_type=item.get("type", "dm"),
|
|
requester_ship=item.get("requester", ""),
|
|
channel_id=item.get("channel", ""),
|
|
message_content=item.get("content", ""),
|
|
created_at=item.get("created_at", 0),
|
|
status=item.get("status", "pending"),
|
|
)
|
|
if approval.status == "pending":
|
|
self._pending[approval.id] = approval
|
|
else:
|
|
self._resolved[approval.id] = approval
|
|
loaded.append(approval)
|
|
if loaded or stored_blocked:
|
|
logger.info(
|
|
f"[Urbit] Loaded {len(loaded)} approvals "
|
|
f"and {len(self._blocked_ships)} blocked ships from Settings Store"
|
|
)
|
|
return loaded
|
|
except Exception as e:
|
|
logger.warning(f"[Urbit] Failed to load approvals: {e}")
|
|
return []
|