新增 Microsoft Teams 渠道扩展,支持在 Yuxi 平台中集成 Microsoft Teams 协作平台。 包含以下功能模块: - sdk: Bot Framework SDK 封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - auth: JWT 认证 - jwks: JWKS 密钥管理 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - state: 状态管理 - runtime: 运行时管理 - actions: 动作处理 - adaptive_card: 自适应卡片 - task_modules: 任务模块 - message_extension: 消息扩展 - proactive: Proactive Messaging - graph: Microsoft Graph API 集成 - graph_teams: Teams 操作 - graph_members: 成员管理 - graph_messages: 消息获取 - graph_thread: 线程管理 - graph_users: 用户管理 - graph_upload: 文件上传 - files: 文件处理 - file_consent: 文件授权 - conversations: 会话存储 - mentions: @提及处理 - threading: 线程管理 - reactions: 表情反应 - polls: 投票功能 - meetings: 会议集成 - feedback: 反馈处理 - sso: 单点登录 - deep_links: 深层链接 - incoming_webhook: 入站 Webhook - localization: 本地化 - user_agent: 用户代理 - sent_message_cache: 消息缓存 - types: 类型定义
188 lines
6.2 KiB
Python
188 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DM_POLICIES = ("pairing", "allowlist", "open", "disabled")
|
|
GROUP_POLICIES = ("open", "allowlist", "disabled")
|
|
REPLY_STYLES = ("thread", "top-level")
|
|
|
|
|
|
@dataclass
|
|
class MSTeamsDmPolicy:
|
|
mode: str = "pairing"
|
|
allow_from: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class MSTeamsGroupPolicy:
|
|
mode: str = "allowlist"
|
|
allow_from: list[str] = field(default_factory=list)
|
|
require_mention: bool = True
|
|
|
|
|
|
@dataclass
|
|
class MSTeamsChannelPolicy:
|
|
require_mention: bool = True
|
|
reply_style: str = "thread"
|
|
tools: dict | None = None
|
|
tools_by_sender: list[dict] | None = None
|
|
|
|
|
|
class MSTeamsSecurity:
|
|
def __init__(self, config: dict | None = None):
|
|
self._config = config or {}
|
|
|
|
def _channel_cfg(self) -> dict:
|
|
return self._config.get("channels", {}).get("msteams", {}) or {}
|
|
|
|
def resolve_dm_policy(self, account: dict | None = None) -> MSTeamsDmPolicy:
|
|
channel_cfg = self._channel_cfg()
|
|
mode = channel_cfg.get("dmPolicy", "pairing")
|
|
allow_from = channel_cfg.get("allowFrom", [])
|
|
if account:
|
|
mode = account.get("dm_policy", mode)
|
|
allow_from = account.get("allow_from", allow_from)
|
|
return MSTeamsDmPolicy(mode=mode, allow_from=allow_from)
|
|
|
|
def resolve_group_policy(
|
|
self,
|
|
account: dict | None = None,
|
|
team_id: str | None = None,
|
|
channel_id: str | None = None,
|
|
) -> MSTeamsGroupPolicy:
|
|
channel_cfg = self._channel_cfg()
|
|
default_mode = channel_cfg.get("groupPolicy", "allowlist")
|
|
default_mention = channel_cfg.get("requireMention", True)
|
|
allow_from = channel_cfg.get("groupAllowFrom", [])
|
|
|
|
teams = channel_cfg.get("teams", {}) or {}
|
|
|
|
if team_id:
|
|
team_cfg = teams.get(team_id, {}) or {}
|
|
if channel_id and team_cfg:
|
|
channels = team_cfg.get("channels", {}) or {}
|
|
ch_cfg = channels.get(channel_id, {}) or {}
|
|
if ch_cfg:
|
|
return MSTeamsGroupPolicy(
|
|
mode=ch_cfg.get("policy", default_mode),
|
|
require_mention=ch_cfg.get("requireMention", team_cfg.get("requireMention", default_mention)),
|
|
)
|
|
return MSTeamsGroupPolicy(
|
|
mode=team_cfg.get("policy", default_mode),
|
|
require_mention=team_cfg.get("requireMention", default_mention),
|
|
)
|
|
|
|
wildcard = teams.get("*", {}) or {}
|
|
return MSTeamsGroupPolicy(
|
|
mode=wildcard.get("policy", default_mode),
|
|
allow_from=allow_from,
|
|
require_mention=wildcard.get("requireMention", default_mention),
|
|
)
|
|
|
|
def is_allowed_dm(self, policy: MSTeamsDmPolicy, peer_id: str) -> tuple[bool, str | None]:
|
|
if policy.mode == "disabled":
|
|
return False, "dm-disabled"
|
|
if policy.mode == "open":
|
|
return True, None
|
|
if policy.mode == "allowlist":
|
|
if _check_allowlist(policy.allow_from, peer_id):
|
|
return True, None
|
|
return False, "not-in-allowlist"
|
|
if policy.mode == "pairing":
|
|
if _check_allowlist(policy.allow_from, peer_id):
|
|
return True, "paired"
|
|
return True, "pairing-required"
|
|
return False, "unknown-policy"
|
|
|
|
def is_allowed_group(
|
|
self,
|
|
policy: MSTeamsGroupPolicy,
|
|
peer_id: str,
|
|
is_mentioned: bool = False,
|
|
) -> tuple[bool, str | None]:
|
|
if policy.mode == "disabled":
|
|
return False, "group-disabled"
|
|
if policy.require_mention and not is_mentioned:
|
|
return False, "mention-required"
|
|
if policy.mode == "open":
|
|
return True, None
|
|
if policy.mode == "allowlist":
|
|
if _check_allowlist(policy.allow_from, peer_id) or not policy.allow_from:
|
|
return True, None
|
|
return False, "not-in-group-allowlist"
|
|
return False, "unknown-policy"
|
|
|
|
def resolve_reply_style(
|
|
self,
|
|
team_id: str | None = None,
|
|
channel_id: str | None = None,
|
|
) -> str:
|
|
channel_cfg = self._channel_cfg()
|
|
default_style = channel_cfg.get("replyStyle", "thread")
|
|
|
|
if not team_id:
|
|
return default_style
|
|
|
|
teams = channel_cfg.get("teams", {}) or {}
|
|
team_cfg = teams.get(team_id, {}) or {}
|
|
|
|
if channel_id:
|
|
channels = team_cfg.get("channels", {}) or {}
|
|
ch_cfg = channels.get(channel_id, {}) or {}
|
|
if "replyStyle" in ch_cfg:
|
|
return ch_cfg["replyStyle"]
|
|
|
|
return team_cfg.get("replyStyle", default_style)
|
|
|
|
def resolve_tools_policy(
|
|
self,
|
|
team_id: str | None = None,
|
|
channel_id: str | None = None,
|
|
sender_peer_id: str | None = None,
|
|
) -> dict:
|
|
channel_cfg = self._channel_cfg()
|
|
default_tools = channel_cfg.get("tools", {}) or {}
|
|
|
|
if not team_id:
|
|
return default_tools
|
|
|
|
teams = channel_cfg.get("teams", {}) or {}
|
|
team_cfg = teams.get(team_id, {}) or {}
|
|
team_tools = team_cfg.get("tools", {}) or {}
|
|
|
|
if channel_id:
|
|
channels = team_cfg.get("channels", {}) or {}
|
|
ch_cfg = channels.get(channel_id, {}) or {}
|
|
ch_tools = ch_cfg.get("tools", {}) or {}
|
|
effective = {**default_tools, **team_tools, **ch_tools}
|
|
else:
|
|
effective = {**default_tools, **team_tools}
|
|
|
|
tools_by_sender = effective.get("toolsBySender", [])
|
|
if sender_peer_id and tools_by_sender:
|
|
for rule in tools_by_sender:
|
|
if rule.get("sender") == sender_peer_id or rule.get("sender") == "*":
|
|
return rule
|
|
|
|
return effective
|
|
|
|
|
|
def _normalize_peer(peer_id: str) -> str:
|
|
for prefix in ("msteams:", "teams:"):
|
|
if peer_id.startswith(prefix):
|
|
return peer_id[len(prefix) :]
|
|
return str(peer_id)
|
|
|
|
|
|
def _check_allowlist(allow_from: list[str], peer_id: str) -> bool:
|
|
if "*" in allow_from:
|
|
return True
|
|
normalized = _normalize_peer(str(peer_id))
|
|
for entry in allow_from:
|
|
if _normalize_peer(str(entry)) == normalized:
|
|
return True
|
|
return False
|