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