Files
2025-10-17 17:48:46 +08:00

145 lines
5.3 KiB
Python

from __future__ import annotations
import hashlib
import hmac
import json
import time
from collections.abc import Mapping
from typing import Any
from events.catalog_data import EVENT_CATALOG
from werkzeug import Request, Response
from dify_plugin.entities.trigger import EventDispatch, Subscription
from dify_plugin.errors.trigger import (
TriggerDispatchError,
TriggerValidationError,
)
from dify_plugin.interfaces.trigger import Trigger
MESSAGE_CHANNEL_EVENT_KEYS: dict[str, str] = {
"app_home": "message_app_home",
"channel": "message_channels",
"group": "message_groups",
"im": "message_im",
"mpim": "message_mpim",
}
MESSAGE_IGNORED_SUBTYPES = {
"message_changed",
"message_deleted",
"message_replied",
"thread_broadcast",
"channel_join",
"channel_leave",
}
EVENT_KEY_BY_TYPE: dict[str, str] = {
str(metadata.get("event_type")): event_key
for event_key, metadata in EVENT_CATALOG.items()
if metadata.get("event_type") != "message"
}
class SlackTrigger(Trigger):
"""Trigger implementation for Slack event webhooks."""
_VERSION = "v0"
_MAX_SIGNATURE_AGE = 60 * 5 # five minutes
def _dispatch_event(self, subscription: Subscription, request: Request) -> EventDispatch:
signing_secret = str(subscription.properties.get("signing_secret") or "")
if not signing_secret:
raise TriggerDispatchError("Slack signing secret is missing from subscription properties")
if request.headers.get("X-Slack-Retry-Num"):
return EventDispatch(
events=[],
response=Response(response="{}", status=200, mimetype="application/json"),
)
payload, response = self._parse_request(signing_secret=signing_secret, request=request)
dispatch_event = self._determine_event(subscription=subscription, payload=payload)
events: list[str] = []
if dispatch_event:
events.append(dispatch_event)
return EventDispatch(events=events, response=response)
def _parse_request(self, signing_secret: str, request: Request) -> tuple[Mapping[str, Any], Response]:
body_bytes = request.get_data(cache=True, as_text=False)
timestamp_header = request.headers.get("X-Slack-Request-Timestamp")
if not timestamp_header:
raise TriggerValidationError("Missing Slack timestamp header")
try:
timestamp = int(timestamp_header)
except ValueError as exc:
raise TriggerValidationError("Invalid Slack timestamp header") from exc
current_time = int(time.time())
if abs(current_time - timestamp) > self._MAX_SIGNATURE_AGE:
raise TriggerValidationError("Slack request timestamp is outside the allowed tolerance")
signature = request.headers.get("X-Slack-Signature")
if not signature:
raise TriggerValidationError("Missing Slack signature header")
try:
body_text = body_bytes.decode("utf-8")
except UnicodeDecodeError as exc:
raise TriggerDispatchError("Slack payload must be UTF-8 encoded") from exc
expected_signature = self._build_signature(
signing_secret=signing_secret,
timestamp=timestamp_header,
body=body_text,
)
if not hmac.compare_digest(signature, expected_signature):
raise TriggerValidationError("Invalid Slack signature")
try:
payload = json.loads(body_text)
except json.JSONDecodeError as exc:
raise TriggerDispatchError("Failed to decode Slack payload") from exc
if payload.get("type") == "url_verification":
challenge = payload.get("challenge")
if not challenge:
raise TriggerDispatchError("Slack URL verification payload missing challenge")
return payload, Response(challenge, mimetype="text/plain", status=200)
return payload, Response(response="{}", status=200, mimetype="application/json")
def _determine_event(self, subscription: Subscription, payload: Mapping[str, Any]) -> str:
if payload.get("type") != "event_callback":
return ""
event = payload.get("event")
if not isinstance(event, Mapping):
raise TriggerDispatchError("Slack payload missing event body")
event_type = str(event.get("type") or "")
if event_type == "message":
subtype = str(event.get("subtype") or "")
if subtype and subtype in MESSAGE_IGNORED_SUBTYPES:
return ""
channel_type = str(event.get("channel_type") or "")
event_key = MESSAGE_CHANNEL_EVENT_KEYS.get(channel_type)
if event_key:
return event_key if event_key in EVENT_CATALOG else ""
return "message" if "message" in EVENT_CATALOG else ""
event_key = EVENT_KEY_BY_TYPE.get(event_type)
if event_key:
return event_key
return event_type if event_type in EVENT_CATALOG else ""
def _build_signature(self, signing_secret: str, timestamp: str, body: str) -> str:
sig_basestring = f"{self._VERSION}:{timestamp}:{body}"
digest = hmac.new(signing_secret.encode("utf-8"), sig_basestring.encode("utf-8"), hashlib.sha256)
return f"{self._VERSION}={digest.hexdigest()}"