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

@@ -65,6 +65,38 @@ class Settings(BaseSettings):
description="Slack app-level token for Socket Mode (xapp-...)",
)
# -------------------------------------------------------------------------
# WhatsApp
# -------------------------------------------------------------------------
whatsapp_app_secret: str = Field(
default="",
description="WhatsApp app secret for HMAC-SHA256 webhook signature verification",
)
whatsapp_verify_token: str = Field(
default="",
description="WhatsApp webhook verification token (hub.verify_token)",
)
# -------------------------------------------------------------------------
# MinIO / Object Storage
# -------------------------------------------------------------------------
minio_endpoint: str = Field(
default="http://localhost:9000",
description="MinIO endpoint URL (S3-compatible)",
)
minio_access_key: str = Field(
default="minioadmin",
description="MinIO access key",
)
minio_secret_key: str = Field(
default="minioadmin",
description="MinIO secret key",
)
minio_media_bucket: str = Field(
default="konstruct-media",
description="MinIO bucket name for media attachments",
)
# -------------------------------------------------------------------------
# LLM Providers
# -------------------------------------------------------------------------

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):