ForcePilot/backend/package/yuxi/channels/adapters/mattermost/interaction_http.py
Kris 002d601d1b feat(mattermost): 实现完整的 Mattermost 适配器模块
新增 Mattermost 渠道完整实现,包含适配器核心、消息处理、交互回调、命令支持、安全校验、多账号管理等功能,支持机器人消息发送、交互按钮、命令注册、投票功能以及配置动态修改等特性。
2026-05-12 00:46:12 +08:00

157 lines
4.9 KiB
Python

from __future__ import annotations
import hashlib
import hmac
import json
import os
from typing import Any
from yuxi.utils.logging_config import logger
def verify_interaction_hmac(body: bytes, signature: str, secret: str) -> bool:
if not secret or not signature:
return False
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
def is_allowed_interaction_source(
remote_ip: str,
allowed_ips: list[str] | None = None,
trusted_proxies: list[str] | None = None,
) -> bool:
if not allowed_ips:
return True
return remote_ip in allowed_ips
async def handle_interaction(
body: bytes,
headers: dict,
remote_ip: str,
adapter: Any,
) -> dict:
signing_secret = os.getenv("MATTERMOST_SIGNING_SECRET", "")
signature = headers.get("X-Mattermost-Signature", "") or headers.get("x-mattermost-signature", "")
if not verify_interaction_hmac(body, signature, signing_secret):
logger.warning("[Mattermost] Interaction HMAC verification failed")
return {"text": "签名验证失败", "status": 401}
config = (adapter.config or {}) if adapter else {}
interactions_cfg = config.get("interactions", {})
allowed_ips = interactions_cfg.get("allowedSourceIps", interactions_cfg.get("allowed_source_ips", []))
if allowed_ips and not is_allowed_interaction_source(remote_ip, allowed_ips):
logger.warning(f"[Mattermost] Interaction from unauthorized IP: {remote_ip}")
return {"text": "来源 IP 未被允许", "status": 403}
try:
data = json.loads(body)
except json.JSONDecodeError:
return {"text": "无效的请求数据", "status": 400}
action_type = data.get("type", "")
context = data.get("context", {})
if isinstance(context, str):
try:
context = json.loads(context)
except json.JSONDecodeError:
context = {}
user_id = data.get("user_id", "")
channel_id = data.get("channel_id", "")
post_id = data.get("post_id", "")
team_id = data.get("team_id", "")
logger.info(
f"[Mattermost] Interaction received: type={action_type} user={user_id} channel={channel_id} team={team_id}"
)
actions_list = data.get("data", {}).get("actions", []) if isinstance(data.get("data"), dict) else []
for action in actions_list:
action_name = action.get("name", "")
action_value = action.get("value", "")
action_context = action.get("integration", {}).get("context", action.get("context", {}))
if isinstance(action_context, str):
try:
action_context = json.loads(action_context)
except json.JSONDecodeError:
action_context = {}
merged_context = {**context, **action_context}
return await _dispatch_action(
adapter, action_name, action_value, user_id, channel_id, post_id, team_id, merged_context
)
return {"update": {"message": "操作已处理", "props": {}}}
async def _dispatch_action(
adapter: Any,
action_name: str,
action_value: str,
user_id: str,
channel_id: str,
post_id: str,
team_id: str,
context: dict,
) -> dict:
if action_name in ("poll_vote", "poll"):
return await _handle_poll_action(adapter, action_name, action_value, user_id, channel_id, post_id, context)
if action_name.startswith("model_select_") or action_name in (
"model_provider_select",
"model_select",
"model_confirm",
):
from .model_picker_interaction import handle_model_picker_interaction
return await handle_model_picker_interaction(
adapter, user_id, channel_id, post_id, action_name, action_value, adapter.config
)
if action_name == "approve_exec":
from .approval import ApprovalManager
mgr = ApprovalManager()
mgr.approve(action_value, user_id)
return {"update": {"message": "执行已批准 ✅", "props": {}}}
if action_name == "deny_exec":
from .approval import ApprovalManager
mgr = ApprovalManager()
mgr.deny(action_value, user_id)
return {"update": {"message": "执行已拒绝 ❌", "props": {}}}
return {"update": {"message": f"操作已处理: {action_name}", "props": {}}}
async def _handle_poll_action(
adapter: Any,
action_name: str,
action_value: str,
user_id: str,
channel_id: str,
post_id: str,
context: dict,
) -> dict:
poll_id = context.get("poll_id", "")
vote = context.get("vote", action_value)
if adapter and hasattr(adapter, "handle_poll_vote"):
result = await adapter.handle_poll_vote(poll_id, vote or action_value, user_id)
if result.get("error"):
return {"update": {"message": f"投票处理失败: {result['error']}", "props": {}}}
return {
"update": {
"message": f"投票已记录: {vote}",
"props": {},
}
}