feat(02-03): add MediaAttachment model, WhatsApp normalizer, and signature verification

- Add MediaType(StrEnum) and MediaAttachment(BaseModel) to shared/models/message.py
- Add media: list[MediaAttachment] field to MessageContent
- Add whatsapp_app_secret, whatsapp_verify_token, and MinIO settings to shared/config.py
- Add normalize_whatsapp_event() to gateway/normalize.py (text, image, document support)
- Create whatsapp.py adapter with verify_whatsapp_signature() and verify_hub_challenge()
- 30 new passing tests (signature verification + normalizer)
This commit is contained in:
2026-03-23 14:41:48 -06:00
parent b2e86f1046
commit 370a860622
7 changed files with 1987 additions and 3 deletions

View File

@@ -28,6 +28,38 @@ class ChannelType(StrEnum):
SIGNAL = "signal"
class MediaType(StrEnum):
"""Supported media attachment types."""
IMAGE = "image"
DOCUMENT = "document"
AUDIO = "audio"
VIDEO = "video"
class MediaAttachment(BaseModel):
"""
A media file attached to a message (image, document, audio, or video).
After normalization, `url` contains a placeholder media ID URL from the
channel's API. The channel adapter downloads the media and stores it in
MinIO, then updates `storage_key` and `url` with the final presigned URL.
"""
media_type: MediaType = Field(description="Type of media: image, document, audio, or video")
url: str | None = Field(
default=None,
description="Download URL — placeholder media ID URL after normalization, presigned MinIO URL after storage",
)
storage_key: str | None = Field(
default=None,
description="MinIO object key: {tenant_id}/{agent_id}/{message_id}/{filename}",
)
mime_type: str | None = Field(default=None, description="MIME type (e.g. image/jpeg)")
filename: str | None = Field(default=None, description="Original filename if available")
size_bytes: int | None = Field(default=None, description="File size in bytes if available")
class SenderInfo(BaseModel):
"""Information about the message sender."""
@@ -50,6 +82,10 @@ class MessageContent(BaseModel):
default_factory=list,
description="List of user/bot IDs mentioned in the message",
)
media: list[MediaAttachment] = Field(
default_factory=list,
description="Typed media attachments (images, documents, audio, video)",
)
class KonstructMessage(BaseModel):