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