mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-21 04:39:06 +00:00
* transcript restart script * fix tests? * remove useless comment --------- Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
229 lines
6.4 KiB
Python
229 lines
6.4 KiB
Python
"""
|
|
Daily.co Webhook Utilities
|
|
|
|
Utilities for verifying and parsing Daily.co webhook events.
|
|
|
|
Reference: https://docs.daily.co/reference/rest-api/webhooks
|
|
"""
|
|
|
|
import base64
|
|
import hmac
|
|
from hashlib import sha256
|
|
|
|
import structlog
|
|
|
|
from .webhooks import (
|
|
DailyWebhookEvent,
|
|
ParticipantJoinedPayload,
|
|
ParticipantLeftPayload,
|
|
RecordingErrorPayload,
|
|
RecordingReadyToDownloadPayload,
|
|
RecordingStartedPayload,
|
|
)
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
|
|
def verify_webhook_signature(
|
|
body: bytes,
|
|
signature: str,
|
|
timestamp: str,
|
|
webhook_secret: str,
|
|
) -> bool:
|
|
"""
|
|
Verify Daily.co webhook signature using HMAC-SHA256.
|
|
|
|
Daily.co signature verification:
|
|
1. Base64-decode the webhook secret
|
|
2. Create signed content: timestamp + '.' + body
|
|
3. Compute HMAC-SHA256(secret, signed_content)
|
|
4. Base64-encode the result
|
|
5. Compare with provided signature using constant-time comparison
|
|
|
|
Reference: https://docs.daily.co/reference/rest-api/webhooks
|
|
|
|
Args:
|
|
body: Raw request body bytes
|
|
signature: X-Webhook-Signature header value
|
|
timestamp: X-Webhook-Timestamp header value
|
|
webhook_secret: Base64-encoded HMAC secret
|
|
|
|
Returns:
|
|
True if signature is valid, False otherwise
|
|
|
|
Example:
|
|
>>> body = b'{"version":"1.0.0","type":"participant.joined",...}'
|
|
>>> signature = "abc123..."
|
|
>>> timestamp = "1234567890"
|
|
>>> secret = "your-base64-secret"
|
|
>>> is_valid = verify_webhook_signature(body, signature, timestamp, secret)
|
|
"""
|
|
if not signature or not timestamp or not webhook_secret:
|
|
logger.warning(
|
|
"Missing required data for webhook verification",
|
|
has_signature=bool(signature),
|
|
has_timestamp=bool(timestamp),
|
|
has_secret=bool(webhook_secret),
|
|
)
|
|
return False
|
|
|
|
try:
|
|
secret_bytes = base64.b64decode(webhook_secret)
|
|
signed_content = timestamp.encode() + b"." + body
|
|
expected = hmac.new(secret_bytes, signed_content, sha256).digest()
|
|
expected_b64 = base64.b64encode(expected).decode()
|
|
|
|
# Constant-time comparison to prevent timing attacks
|
|
return hmac.compare_digest(expected_b64, signature)
|
|
|
|
except (base64.binascii.Error, ValueError, TypeError, UnicodeDecodeError) as e:
|
|
logger.error(
|
|
"Webhook signature verification failed",
|
|
error=str(e),
|
|
error_type=type(e).__name__,
|
|
)
|
|
return False
|
|
|
|
|
|
def extract_room_name(event: DailyWebhookEvent) -> str | None:
|
|
"""
|
|
Extract room name from Daily.co webhook event payload.
|
|
|
|
Args:
|
|
event: Parsed webhook event
|
|
|
|
Returns:
|
|
Room name if present and is a string, None otherwise
|
|
|
|
Example:
|
|
>>> event = DailyWebhookEvent(**webhook_payload)
|
|
>>> room_name = extract_room_name(event)
|
|
"""
|
|
room = event.payload.get("room_name")
|
|
# Ensure we return a string, not any falsy value that might be in payload
|
|
return room if isinstance(room, str) else None
|
|
|
|
|
|
def parse_participant_joined(event: DailyWebhookEvent) -> ParticipantJoinedPayload:
|
|
"""
|
|
Parse participant.joined webhook event payload.
|
|
|
|
Args:
|
|
event: Webhook event with type "participant.joined"
|
|
|
|
Returns:
|
|
Parsed participant joined payload
|
|
|
|
Raises:
|
|
pydantic.ValidationError: If payload doesn't match expected schema
|
|
"""
|
|
return ParticipantJoinedPayload(**event.payload)
|
|
|
|
|
|
def parse_participant_left(event: DailyWebhookEvent) -> ParticipantLeftPayload:
|
|
"""
|
|
Parse participant.left webhook event payload.
|
|
|
|
Args:
|
|
event: Webhook event with type "participant.left"
|
|
|
|
Returns:
|
|
Parsed participant left payload
|
|
|
|
Raises:
|
|
pydantic.ValidationError: If payload doesn't match expected schema
|
|
"""
|
|
return ParticipantLeftPayload(**event.payload)
|
|
|
|
|
|
def parse_recording_started(event: DailyWebhookEvent) -> RecordingStartedPayload:
|
|
"""
|
|
Parse recording.started webhook event payload.
|
|
|
|
Args:
|
|
event: Webhook event with type "recording.started"
|
|
|
|
Returns:
|
|
Parsed recording started payload
|
|
|
|
Raises:
|
|
pydantic.ValidationError: If payload doesn't match expected schema
|
|
"""
|
|
return RecordingStartedPayload(**event.payload)
|
|
|
|
|
|
def parse_recording_ready(
|
|
event: DailyWebhookEvent,
|
|
) -> RecordingReadyToDownloadPayload:
|
|
"""
|
|
Parse recording.ready-to-download webhook event payload.
|
|
|
|
This event is sent when raw-tracks recordings are complete and uploaded to S3.
|
|
The payload includes a 'tracks' array with individual audio/video files.
|
|
|
|
Args:
|
|
event: Webhook event with type "recording.ready-to-download"
|
|
|
|
Returns:
|
|
Parsed recording ready payload with tracks array
|
|
|
|
Raises:
|
|
pydantic.ValidationError: If payload doesn't match expected schema
|
|
|
|
Example:
|
|
>>> event = DailyWebhookEvent(**webhook_payload)
|
|
>>> if event.type == "recording.ready-to-download":
|
|
... payload = parse_recording_ready(event)
|
|
... audio_tracks = [t for t in payload.tracks if t.type == "audio"]
|
|
"""
|
|
return RecordingReadyToDownloadPayload(**event.payload)
|
|
|
|
|
|
def parse_recording_error(event: DailyWebhookEvent) -> RecordingErrorPayload:
|
|
"""
|
|
Parse recording.error webhook event payload.
|
|
|
|
Args:
|
|
event: Webhook event with type "recording.error"
|
|
|
|
Returns:
|
|
Parsed recording error payload
|
|
|
|
Raises:
|
|
pydantic.ValidationError: If payload doesn't match expected schema
|
|
"""
|
|
return RecordingErrorPayload(**event.payload)
|
|
|
|
|
|
WEBHOOK_PARSERS = {
|
|
"participant.joined": parse_participant_joined,
|
|
"participant.left": parse_participant_left,
|
|
"recording.started": parse_recording_started,
|
|
"recording.ready-to-download": parse_recording_ready,
|
|
"recording.error": parse_recording_error,
|
|
}
|
|
|
|
|
|
def parse_webhook_payload(event: DailyWebhookEvent):
|
|
"""
|
|
Parse webhook event payload based on event type.
|
|
|
|
Args:
|
|
event: Webhook event
|
|
|
|
Returns:
|
|
Typed payload model based on event type, or raw dict if unknown
|
|
|
|
Example:
|
|
>>> event = DailyWebhookEvent(**webhook_payload)
|
|
>>> payload = parse_webhook_payload(event)
|
|
>>> if isinstance(payload, ParticipantJoinedPayload):
|
|
... print(f"User {payload.user_name} joined")
|
|
"""
|
|
parser = WEBHOOK_PARSERS.get(event.type)
|
|
if parser:
|
|
return parser(event)
|
|
else:
|
|
logger.warning("Unknown webhook event type", event_type=event.type)
|
|
return event.payload
|