- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
147 lines
4.6 KiB
Python
147 lines
4.6 KiB
Python
"""
|
|
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,
|
|
}
|