ForcePilot/backend/package/yuxi/channels/adapters/mattermost/interactions.py
Kris d7fe152dae feat(mattermost): 完成 Mattermost 适配器多账号支持与功能增强
本次提交对 Mattermost 适配器进行了全面升级:
1.  重构为多账号架构,支持同时管理多个 Mattermost 机器人账号
2.  更新安全策略默认配置为配对模式和白名单模式
3.  新增 WebSocket 心跳、重连配置项与连接监控
4.  扩展 Agent 工具支持 pin/unpin、获取反应、搜索消息等操作
5.  重构交互按钮构建逻辑,新增分页与提供商筛选功能
6.  优化 SSRF 防护代码,复用公共工具库实现
7.  新增配置兼容性迁移与可变白名单项检测
8.  完善错误处理与日志输出,添加重复消息去重统计
9.  新增发送临时消息(ephemeral)支持
10. 修复提及检测逻辑,正确处理用户名大小写
2026-05-13 16:12:02 +08:00

140 lines
3.9 KiB
Python

from __future__ import annotations
import hashlib
import hmac
import json
import re
from typing import Any
def sanitize_action_id(action_id: str) -> str:
return re.sub(r"[-_]", "", action_id)
def build_button_attachment(
title: str,
text: str,
buttons: list[dict[str, str]],
fallback: str = "",
color: str = "",
callback_id: str = "",
) -> dict[str, Any]:
"""构建 Mattermost Interactive Button attachment。"""
actions = []
for i, btn in enumerate(buttons):
btn_id = sanitize_action_id(btn.get("id", f"btn_{i}"))
actions.append(
{
"id": btn_id,
"name": btn.get("name", f"选择 {i + 1}"),
"integration": {
"url": btn.get("callback_url", ""),
"context": {
"action": btn.get("action", ""),
"button_id": btn_id,
"callback_id": sanitize_action_id(callback_id),
"value": btn.get("value", ""),
},
},
"type": "button",
"text": btn.get("text", f"Button {i + 1}"),
"style": btn.get("style", ""),
}
)
return {
"fallback": fallback or title,
"title": title,
"text": text,
"color": color or "",
"callback_id": callback_id,
"actions": actions,
}
def build_select_attachment(
title: str,
text: str,
options: list[dict[str, str]],
select_name: str = "selection",
callback_id: str = "",
) -> dict[str, Any]:
"""构建 Mattermost Select dropdown attachment。"""
select_options = []
for opt in options:
select_options.append(
{
"text": opt.get("text", ""),
"value": opt.get("value", ""),
}
)
return {
"fallback": title,
"title": title,
"text": text,
"callback_id": sanitize_action_id(callback_id),
"actions": [
{
"id": sanitize_action_id(select_name),
"name": select_name,
"integration": {
"url": "",
"context": {"action": select_name, "callback_id": sanitize_action_id(callback_id)},
},
"type": "select",
"options": select_options,
},
],
}
def parse_interaction_context(data: dict) -> dict:
"""解析交互回调 context。"""
context = data.get("context", {})
if isinstance(context, str):
try:
context = json.loads(context)
except json.JSONDecodeError:
context = {}
return context
def verify_hmac_signature(payload: bytes, signature: str, secret: str) -> bool:
"""验证 HMAC 签名。"""
if not signature or not secret:
return False
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
def build_button_props_pipeline(
channel_id: str,
buttons: list[dict[str, Any]],
flatten: bool = True,
) -> list[dict[str, Any]]:
result: list[dict[str, Any]] = []
for item in buttons:
if flatten and isinstance(item, list):
for nested in item:
_bind_channel_to_button(nested, channel_id)
result.append(nested)
else:
_bind_channel_to_button(item, channel_id)
result.append(item)
return result
def _bind_channel_to_button(button: dict[str, Any], channel_id: str) -> None:
integration = button.get("integration", {})
if integration:
context = integration.get("context", {})
if context:
context["channel_id"] = channel_id
integration["context"] = context
button["integration"] = integration