新增 Mattermost 渠道完整实现,包含适配器核心、消息处理、交互回调、命令支持、安全校验、多账号管理等功能,支持机器人消息发送、交互按钮、命令注册、投票功能以及配置动态修改等特性。
157 lines
4.9 KiB
Python
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": {},
|
|
}
|
|
}
|