实现了 Teams 机器人所需的全功能组件,包括: - 基础命令解析与帮助卡片生成 - 租户验证与访问控制 - 自定义 UA 与媒体工具 - 消息分块、批注处理与会话管理 - 防抖、缓存与配置路由能力 - 投票、配对、审计与运行时状态管理 - TTS 语音合成与卡片构建工具 - 群组管理与权限控制逻辑
159 lines
4.9 KiB
Python
159 lines
4.9 KiB
Python
"""Microsoft Teams Pairing Request 流程。
|
|
|
|
当未在白名单用户向 Bot 发送 DM 时,创建配对请求卡片。
|
|
管理员可通过命令审批配对请求,审批后用户被添加到已配对集合。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
PAIRING_TTL_SECONDS = 24 * 3600
|
|
PAIRING_STORE_FILENAME = "msteams-pairing.json"
|
|
|
|
|
|
class PairingManager:
|
|
def __init__(self, storage_dir: str | None = None):
|
|
self._storage_dir = Path(storage_dir or str(Path.home() / ".yuxi" / "msteams"))
|
|
self._storage_dir.mkdir(parents=True, exist_ok=True)
|
|
self._pending: dict[str, dict[str, Any]] = {}
|
|
self._approved: set[str] = set()
|
|
self._load()
|
|
|
|
@property
|
|
def file_path(self) -> Path:
|
|
return self._storage_dir / PAIRING_STORE_FILENAME
|
|
|
|
def _load(self) -> None:
|
|
if not self.file_path.exists():
|
|
return
|
|
try:
|
|
data = json.loads(self.file_path.read_text(encoding="utf-8"))
|
|
except (json.JSONDecodeError, OSError):
|
|
return
|
|
self._pending = data.get("pending", {})
|
|
self._approved = set(data.get("approved", []))
|
|
|
|
def _save(self) -> None:
|
|
try:
|
|
self.file_path.write_text(
|
|
json.dumps(
|
|
{"pending": self._pending, "approved": list(self._approved)},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
except OSError as e:
|
|
logger.error(f"MSTeams pairing: failed to save: {e}")
|
|
|
|
def request_pairing(self, user_id: str, user_name: str = "", chat_id: str = "") -> str:
|
|
self._cleanup_expired()
|
|
now = time.time()
|
|
pair_id = _make_pair_id(user_id, now)
|
|
self._pending[pair_id] = {
|
|
"pair_id": pair_id,
|
|
"user_id": user_id,
|
|
"user_name": user_name,
|
|
"chat_id": chat_id,
|
|
"requested_at": now,
|
|
}
|
|
self._save()
|
|
logger.info(f"MSTeams pairing request: {user_name} ({user_id}) -> {pair_id}")
|
|
return pair_id
|
|
|
|
def approve(self, pair_id: str) -> bool:
|
|
if pair_id not in self._pending:
|
|
return False
|
|
user_id = self._pending[pair_id]["user_id"]
|
|
self._approved.add(user_id)
|
|
self._pending.pop(pair_id, None)
|
|
self._save()
|
|
logger.info(f"MSTeams pairing approved: {user_id} ({pair_id})")
|
|
return True
|
|
|
|
def deny(self, pair_id: str) -> bool:
|
|
if pair_id not in self._pending:
|
|
return False
|
|
user_id = self._pending[pair_id]["user_id"]
|
|
self._pending.pop(pair_id, None)
|
|
self._save()
|
|
logger.info(f"MSTeams pairing denied: {user_id} ({pair_id})")
|
|
return True
|
|
|
|
def is_approved(self, user_id: str) -> bool:
|
|
return user_id in self._approved
|
|
|
|
def is_pending(self, user_id: str) -> bool:
|
|
return any(p.get("user_id") == user_id for p in self._pending.values())
|
|
|
|
def get_pending_requests(self) -> list[dict[str, Any]]:
|
|
self._cleanup_expired()
|
|
return list(self._pending.values())
|
|
|
|
def _cleanup_expired(self) -> None:
|
|
now = time.time()
|
|
expired = [pid for pid, req in self._pending.items() if now - req.get("requested_at", 0) > PAIRING_TTL_SECONDS]
|
|
for pid in expired:
|
|
self._pending.pop(pid, None)
|
|
|
|
def cleanup(self) -> int:
|
|
self._cleanup_expired()
|
|
self._save()
|
|
return len(self._pending)
|
|
|
|
|
|
def _make_pair_id(user_id: str, timestamp: float) -> str:
|
|
return f"req_{user_id[:12]}_{int(timestamp)}"
|
|
|
|
|
|
def build_pairing_request_card(
|
|
user_name: str,
|
|
user_id: str,
|
|
pair_id: str,
|
|
bot_name: str = "ForcePilot",
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"type": "AdaptiveCard",
|
|
"version": "1.5",
|
|
"body": [
|
|
{
|
|
"type": "TextBlock",
|
|
"size": "Large",
|
|
"weight": "Bolder",
|
|
"text": "配对请求",
|
|
},
|
|
{
|
|
"type": "TextBlock",
|
|
"text": f"用户 **{user_name}** ({user_id}) 请求与 {bot_name} 配对。",
|
|
"wrap": True,
|
|
},
|
|
{
|
|
"type": "FactSet",
|
|
"facts": [
|
|
{"title": "用户 ID", "value": user_id},
|
|
{"title": "配对 ID", "value": pair_id},
|
|
],
|
|
},
|
|
],
|
|
"actions": [
|
|
{
|
|
"type": "Action.Submit",
|
|
"title": "批准",
|
|
"style": "positive",
|
|
"data": {"action": "pairing_approve", "pair_id": pair_id},
|
|
},
|
|
{
|
|
"type": "Action.Submit",
|
|
"title": "拒绝",
|
|
"style": "destructive",
|
|
"data": {"action": "pairing_deny", "pair_id": pair_id},
|
|
},
|
|
],
|
|
}
|