""" Slack平台适配器 """ import hashlib import hmac import json import logging import time from typing import Any from ..platform_adapter import register_platform_adapter, BasePlatformAdapter, PlatformMetadata logger = logging.getLogger(__name__) @register_platform_adapter( name="slack", display_name="Slack", description="Slack Events API消息适配器,支持文本消息和URL验证", support_streaming=True, support_image=True, support_file=True, support_voice=False, ) class SlackPlatformAdapter(BasePlatformAdapter): """Slack平台适配器 - 签名验证 + challenge响应""" def get_metadata(self) -> PlatformMetadata: from ..platform_adapter import platform_registry return platform_registry["slack"] def verify_signature(self, payload: dict, signature: str, timestamp: str) -> bool: """Slack签名验证: v0=HMAC-SHA256(SigningSecret, 'v0:timestamp:body')""" if not signature or not timestamp: return True # 无签名默认通过 signing_secret = self.config.get("signing_secret", "") if not signing_secret: logger.warning("[Slack] 未配置signing_secret,跳过签名验证") return True try: # Slack签名格式: v0=XXXX # 签名基础字符串: v0:timestamp:body body_str = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) sig_basestring = f"v0:{timestamp}:{body_str}" computed_sig = "v0=" + hmac.new( signing_secret.encode("utf-8"), sig_basestring.encode("utf-8"), hashlib.sha256, ).hexdigest() return hmac.compare_digest(computed_sig, signature) except Exception as e: logger.error(f"[Slack] 签名验证异常: {e}") return False def parse_webhook(self, payload: dict) -> dict: """解析Slack webhook payload为统一消息格式""" # Slack URL验证 challenge 在外部处理 event = payload.get("event", {}) event_type = event.get("type", "message") subtype = event.get("subtype", "") content = "" content_type = "text" sender_id = "" sender_name = "" message_id = "" group_id = None group_name = None if event_type == "message": text = event.get("text", "") user = event.get("user", "") channel = event.get("channel", "") ts = event.get("ts", "") content = text content_type = "text" sender_id = user sender_name = event.get("user_profile", {}).get("display_name", user) message_id = ts # 区分私聊/群聊 channel_type = event.get("channel_type", "") if channel or channel_type == "channel": group_id = channel elif channel: group_id = channel elif event_type == "app_mention": text = event.get("text", "") user = event.get("user", "") channel = event.get("channel", "") ts = event.get("ts", "") # 去除@bot标记 content = text content_type = "text" sender_id = user sender_name = user message_id = ts group_id = channel else: content = json.dumps(event, ensure_ascii=False) content_type = event_type return { "content_type": content_type, "content": content, "sender_id": sender_id, "sender_name": sender_name, "message_id": message_id, "timestamp": int(float(message_id.split(".")[0]) if message_id else time.time()), "group_id": group_id, "group_name": group_name, "raw": payload, } def build_reply(self, channel_config: dict, content: str, reply_to: str = None) -> dict: """构建Slack回复消息""" bot_token = channel_config.get("bot_token", "") channel = channel_config.get("channel", "") if not bot_token: logger.warning("[Slack] 未配置bot_token,无法构建回复") return {} body = { "channel": channel, "text": content, } if reply_to: body["thread_ts"] = reply_to return { "api_url": "https://slack.com/api/chat.postMessage", "headers": { "Content-Type": "application/json", "Authorization": f"Bearer {bot_token}", }, "body": body, }