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 True expected = hmac.new( secret.encode(), payload, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, signature) def build_interactive_post_props( channel_id: str, message: str, attachments: list[dict] | None = None, root_id: str = "", ) -> dict[str, Any]: """构建带 interactive attachments 的 post 选项。""" return { "channel_id": channel_id, "message": message, "root_id": root_id, "props": {"attachments": attachments or []}, }