wappa 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wappa might be problematic. Click here for more details.
- wappa/__init__.py +85 -0
- wappa/api/__init__.py +1 -0
- wappa/api/controllers/__init__.py +10 -0
- wappa/api/controllers/webhook_controller.py +441 -0
- wappa/api/dependencies/__init__.py +15 -0
- wappa/api/dependencies/whatsapp_dependencies.py +220 -0
- wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
- wappa/api/middleware/__init__.py +7 -0
- wappa/api/middleware/error_handler.py +158 -0
- wappa/api/middleware/owner.py +99 -0
- wappa/api/middleware/request_logging.py +184 -0
- wappa/api/routes/__init__.py +6 -0
- wappa/api/routes/health.py +102 -0
- wappa/api/routes/webhooks.py +211 -0
- wappa/api/routes/whatsapp/__init__.py +15 -0
- wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
- wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
- wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
- wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
- wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
- wappa/api/routes/whatsapp_combined.py +35 -0
- wappa/cli/__init__.py +9 -0
- wappa/cli/main.py +199 -0
- wappa/core/__init__.py +6 -0
- wappa/core/config/__init__.py +5 -0
- wappa/core/config/settings.py +161 -0
- wappa/core/events/__init__.py +41 -0
- wappa/core/events/default_handlers.py +642 -0
- wappa/core/events/event_dispatcher.py +244 -0
- wappa/core/events/event_handler.py +247 -0
- wappa/core/events/webhook_factory.py +219 -0
- wappa/core/factory/__init__.py +15 -0
- wappa/core/factory/plugin.py +68 -0
- wappa/core/factory/wappa_builder.py +326 -0
- wappa/core/logging/__init__.py +5 -0
- wappa/core/logging/context.py +100 -0
- wappa/core/logging/logger.py +343 -0
- wappa/core/plugins/__init__.py +34 -0
- wappa/core/plugins/auth_plugin.py +169 -0
- wappa/core/plugins/cors_plugin.py +128 -0
- wappa/core/plugins/custom_middleware_plugin.py +182 -0
- wappa/core/plugins/database_plugin.py +235 -0
- wappa/core/plugins/rate_limit_plugin.py +183 -0
- wappa/core/plugins/redis_plugin.py +224 -0
- wappa/core/plugins/wappa_core_plugin.py +261 -0
- wappa/core/plugins/webhook_plugin.py +253 -0
- wappa/core/types.py +108 -0
- wappa/core/wappa_app.py +546 -0
- wappa/database/__init__.py +18 -0
- wappa/database/adapter.py +107 -0
- wappa/database/adapters/__init__.py +17 -0
- wappa/database/adapters/mysql_adapter.py +187 -0
- wappa/database/adapters/postgresql_adapter.py +169 -0
- wappa/database/adapters/sqlite_adapter.py +174 -0
- wappa/domain/__init__.py +28 -0
- wappa/domain/builders/__init__.py +5 -0
- wappa/domain/builders/message_builder.py +189 -0
- wappa/domain/entities/__init__.py +5 -0
- wappa/domain/enums/messenger_platform.py +123 -0
- wappa/domain/factories/__init__.py +6 -0
- wappa/domain/factories/media_factory.py +450 -0
- wappa/domain/factories/message_factory.py +497 -0
- wappa/domain/factories/messenger_factory.py +244 -0
- wappa/domain/interfaces/__init__.py +32 -0
- wappa/domain/interfaces/base_repository.py +94 -0
- wappa/domain/interfaces/cache_factory.py +85 -0
- wappa/domain/interfaces/cache_interface.py +199 -0
- wappa/domain/interfaces/expiry_repository.py +68 -0
- wappa/domain/interfaces/media_interface.py +311 -0
- wappa/domain/interfaces/messaging_interface.py +523 -0
- wappa/domain/interfaces/pubsub_repository.py +151 -0
- wappa/domain/interfaces/repository_factory.py +108 -0
- wappa/domain/interfaces/shared_state_repository.py +122 -0
- wappa/domain/interfaces/state_repository.py +123 -0
- wappa/domain/interfaces/tables_repository.py +215 -0
- wappa/domain/interfaces/user_repository.py +114 -0
- wappa/domain/interfaces/webhooks/__init__.py +1 -0
- wappa/domain/models/media_result.py +110 -0
- wappa/domain/models/platforms/__init__.py +15 -0
- wappa/domain/models/platforms/platform_config.py +104 -0
- wappa/domain/services/__init__.py +11 -0
- wappa/domain/services/tenant_credentials_service.py +56 -0
- wappa/messaging/__init__.py +7 -0
- wappa/messaging/whatsapp/__init__.py +1 -0
- wappa/messaging/whatsapp/client/__init__.py +5 -0
- wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
- wappa/messaging/whatsapp/handlers/__init__.py +13 -0
- wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
- wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
- wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
- wappa/messaging/whatsapp/messenger/__init__.py +5 -0
- wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
- wappa/messaging/whatsapp/models/__init__.py +61 -0
- wappa/messaging/whatsapp/models/basic_models.py +65 -0
- wappa/messaging/whatsapp/models/interactive_models.py +287 -0
- wappa/messaging/whatsapp/models/media_models.py +215 -0
- wappa/messaging/whatsapp/models/specialized_models.py +304 -0
- wappa/messaging/whatsapp/models/template_models.py +261 -0
- wappa/persistence/cache_factory.py +93 -0
- wappa/persistence/json/__init__.py +14 -0
- wappa/persistence/json/cache_adapters.py +271 -0
- wappa/persistence/json/handlers/__init__.py +1 -0
- wappa/persistence/json/handlers/state_handler.py +250 -0
- wappa/persistence/json/handlers/table_handler.py +263 -0
- wappa/persistence/json/handlers/user_handler.py +213 -0
- wappa/persistence/json/handlers/utils/__init__.py +1 -0
- wappa/persistence/json/handlers/utils/file_manager.py +153 -0
- wappa/persistence/json/handlers/utils/key_factory.py +11 -0
- wappa/persistence/json/handlers/utils/serialization.py +121 -0
- wappa/persistence/json/json_cache_factory.py +76 -0
- wappa/persistence/json/storage_manager.py +285 -0
- wappa/persistence/memory/__init__.py +14 -0
- wappa/persistence/memory/cache_adapters.py +271 -0
- wappa/persistence/memory/handlers/__init__.py +1 -0
- wappa/persistence/memory/handlers/state_handler.py +250 -0
- wappa/persistence/memory/handlers/table_handler.py +280 -0
- wappa/persistence/memory/handlers/user_handler.py +213 -0
- wappa/persistence/memory/handlers/utils/__init__.py +1 -0
- wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
- wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
- wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
- wappa/persistence/memory/memory_cache_factory.py +76 -0
- wappa/persistence/memory/storage_manager.py +235 -0
- wappa/persistence/redis/README.md +699 -0
- wappa/persistence/redis/__init__.py +11 -0
- wappa/persistence/redis/cache_adapters.py +285 -0
- wappa/persistence/redis/ops.py +880 -0
- wappa/persistence/redis/redis_cache_factory.py +71 -0
- wappa/persistence/redis/redis_client.py +231 -0
- wappa/persistence/redis/redis_handler/__init__.py +26 -0
- wappa/persistence/redis/redis_handler/state_handler.py +176 -0
- wappa/persistence/redis/redis_handler/table.py +158 -0
- wappa/persistence/redis/redis_handler/user.py +138 -0
- wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
- wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
- wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
- wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
- wappa/persistence/redis/redis_manager.py +189 -0
- wappa/processors/__init__.py +6 -0
- wappa/processors/base_processor.py +262 -0
- wappa/processors/factory.py +550 -0
- wappa/processors/whatsapp_processor.py +810 -0
- wappa/schemas/__init__.py +6 -0
- wappa/schemas/core/__init__.py +71 -0
- wappa/schemas/core/base_message.py +499 -0
- wappa/schemas/core/base_status.py +322 -0
- wappa/schemas/core/base_webhook.py +312 -0
- wappa/schemas/core/types.py +253 -0
- wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
- wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
- wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
- wappa/schemas/factory.py +754 -0
- wappa/schemas/webhooks/__init__.py +3 -0
- wappa/schemas/whatsapp/__init__.py +6 -0
- wappa/schemas/whatsapp/base_models.py +285 -0
- wappa/schemas/whatsapp/message_types/__init__.py +93 -0
- wappa/schemas/whatsapp/message_types/audio.py +350 -0
- wappa/schemas/whatsapp/message_types/button.py +267 -0
- wappa/schemas/whatsapp/message_types/contact.py +464 -0
- wappa/schemas/whatsapp/message_types/document.py +421 -0
- wappa/schemas/whatsapp/message_types/errors.py +195 -0
- wappa/schemas/whatsapp/message_types/image.py +424 -0
- wappa/schemas/whatsapp/message_types/interactive.py +430 -0
- wappa/schemas/whatsapp/message_types/location.py +416 -0
- wappa/schemas/whatsapp/message_types/order.py +372 -0
- wappa/schemas/whatsapp/message_types/reaction.py +271 -0
- wappa/schemas/whatsapp/message_types/sticker.py +328 -0
- wappa/schemas/whatsapp/message_types/system.py +317 -0
- wappa/schemas/whatsapp/message_types/text.py +411 -0
- wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
- wappa/schemas/whatsapp/message_types/video.py +344 -0
- wappa/schemas/whatsapp/status_models.py +479 -0
- wappa/schemas/whatsapp/validators.py +454 -0
- wappa/schemas/whatsapp/webhook_container.py +438 -0
- wappa/webhooks/__init__.py +17 -0
- wappa/webhooks/core/__init__.py +71 -0
- wappa/webhooks/core/base_message.py +499 -0
- wappa/webhooks/core/base_status.py +322 -0
- wappa/webhooks/core/base_webhook.py +312 -0
- wappa/webhooks/core/types.py +253 -0
- wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
- wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
- wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
- wappa/webhooks/factory.py +754 -0
- wappa/webhooks/whatsapp/__init__.py +6 -0
- wappa/webhooks/whatsapp/base_models.py +285 -0
- wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
- wappa/webhooks/whatsapp/message_types/audio.py +350 -0
- wappa/webhooks/whatsapp/message_types/button.py +267 -0
- wappa/webhooks/whatsapp/message_types/contact.py +464 -0
- wappa/webhooks/whatsapp/message_types/document.py +421 -0
- wappa/webhooks/whatsapp/message_types/errors.py +195 -0
- wappa/webhooks/whatsapp/message_types/image.py +424 -0
- wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
- wappa/webhooks/whatsapp/message_types/location.py +416 -0
- wappa/webhooks/whatsapp/message_types/order.py +372 -0
- wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
- wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
- wappa/webhooks/whatsapp/message_types/system.py +317 -0
- wappa/webhooks/whatsapp/message_types/text.py +411 -0
- wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
- wappa/webhooks/whatsapp/message_types/video.py +344 -0
- wappa/webhooks/whatsapp/status_models.py +479 -0
- wappa/webhooks/whatsapp/validators.py +454 -0
- wappa/webhooks/whatsapp/webhook_container.py +438 -0
- wappa-0.1.0.dist-info/METADATA +269 -0
- wappa-0.1.0.dist-info/RECORD +211 -0
- wappa-0.1.0.dist-info/WHEEL +4 -0
- wappa-0.1.0.dist-info/entry_points.txt +2 -0
- wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""WhatsApp models package."""
|
|
2
|
+
|
|
3
|
+
from .basic_models import BasicTextMessage, MessageResult, ReadStatusMessage
|
|
4
|
+
from .interactive_models import ButtonMessage, CTAMessage, ListMessage
|
|
5
|
+
from .media_models import (
|
|
6
|
+
AudioMessage,
|
|
7
|
+
DocumentMessage,
|
|
8
|
+
ImageMessage,
|
|
9
|
+
MediaType,
|
|
10
|
+
StickerMessage,
|
|
11
|
+
VideoMessage,
|
|
12
|
+
)
|
|
13
|
+
from .specialized_models import (
|
|
14
|
+
BusinessContact,
|
|
15
|
+
ContactCard,
|
|
16
|
+
ContactMessage,
|
|
17
|
+
ContactValidationResult,
|
|
18
|
+
LocationMessage,
|
|
19
|
+
LocationRequestMessage,
|
|
20
|
+
LocationValidationResult,
|
|
21
|
+
PersonalContact,
|
|
22
|
+
)
|
|
23
|
+
from .template_models import (
|
|
24
|
+
LocationTemplateMessage,
|
|
25
|
+
MediaTemplateMessage,
|
|
26
|
+
TemplateMessageStatus,
|
|
27
|
+
TemplateParameter,
|
|
28
|
+
TemplateType,
|
|
29
|
+
TemplateValidationResult,
|
|
30
|
+
TextTemplateMessage,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"MessageResult",
|
|
35
|
+
"BasicTextMessage",
|
|
36
|
+
"ReadStatusMessage",
|
|
37
|
+
"ButtonMessage",
|
|
38
|
+
"CTAMessage",
|
|
39
|
+
"ListMessage",
|
|
40
|
+
"MediaType",
|
|
41
|
+
"ImageMessage",
|
|
42
|
+
"VideoMessage",
|
|
43
|
+
"AudioMessage",
|
|
44
|
+
"DocumentMessage",
|
|
45
|
+
"StickerMessage",
|
|
46
|
+
"ContactCard",
|
|
47
|
+
"ContactMessage",
|
|
48
|
+
"ContactValidationResult",
|
|
49
|
+
"LocationMessage",
|
|
50
|
+
"LocationRequestMessage",
|
|
51
|
+
"LocationValidationResult",
|
|
52
|
+
"BusinessContact",
|
|
53
|
+
"PersonalContact",
|
|
54
|
+
"TemplateType",
|
|
55
|
+
"TemplateParameter",
|
|
56
|
+
"TextTemplateMessage",
|
|
57
|
+
"MediaTemplateMessage",
|
|
58
|
+
"LocationTemplateMessage",
|
|
59
|
+
"TemplateMessageStatus",
|
|
60
|
+
"TemplateValidationResult",
|
|
61
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic message models for WhatsApp messaging.
|
|
3
|
+
|
|
4
|
+
Pydantic schemas for basic messaging operations: send_text and mark_as_read.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from wappa.schemas.core.types import PlatformType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MessageResult(BaseModel):
|
|
15
|
+
"""Result of a messaging operation.
|
|
16
|
+
|
|
17
|
+
Standard response model for all messaging operations across platforms.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
success: bool
|
|
21
|
+
platform: PlatformType = PlatformType.WHATSAPP
|
|
22
|
+
message_id: str | None = None
|
|
23
|
+
recipient: str | None = None
|
|
24
|
+
error: str | None = None
|
|
25
|
+
error_code: str | None = None
|
|
26
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
|
27
|
+
tenant_id: str | None = None # phone_number_id in WhatsApp context
|
|
28
|
+
|
|
29
|
+
class Config:
|
|
30
|
+
use_enum_values = True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BasicTextMessage(BaseModel):
|
|
34
|
+
"""Basic text message schema for send_text operations.
|
|
35
|
+
|
|
36
|
+
Schema for sending text messages with optional reply and preview control.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
text: str = Field(
|
|
40
|
+
..., min_length=1, max_length=4096, description="Text content of the message"
|
|
41
|
+
)
|
|
42
|
+
recipient: str = Field(
|
|
43
|
+
..., min_length=1, description="Recipient phone number or user identifier"
|
|
44
|
+
)
|
|
45
|
+
reply_to_message_id: str | None = Field(
|
|
46
|
+
None, description="Message ID to reply to (creates a thread)"
|
|
47
|
+
)
|
|
48
|
+
disable_preview: bool = Field(
|
|
49
|
+
False, description="Disable URL preview for links in the message"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ReadStatusMessage(BaseModel):
|
|
54
|
+
"""Read status message schema for mark_as_read operations.
|
|
55
|
+
|
|
56
|
+
Schema for marking messages as read with optional typing indicator.
|
|
57
|
+
Key requirement: typing boolean parameter for showing typing indicator.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
message_id: str = Field(
|
|
61
|
+
..., min_length=1, description="WhatsApp message ID to mark as read"
|
|
62
|
+
)
|
|
63
|
+
typing: bool = Field(
|
|
64
|
+
False, description="Show typing indicator when marking as read"
|
|
65
|
+
)
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive message models for WhatsApp messaging.
|
|
3
|
+
|
|
4
|
+
Pydantic schemas for interactive messaging operations based on WhatsApp Cloud API 2025
|
|
5
|
+
specifications and existing interactive_message.py implementation patterns.
|
|
6
|
+
|
|
7
|
+
Supports three types of interactive messages:
|
|
8
|
+
1. Button Messages - Quick reply buttons (max 3)
|
|
9
|
+
2. List Messages - Sectioned lists with rows (max 10 sections, 10 rows each)
|
|
10
|
+
3. Call-to-Action Messages - URL buttons with external links
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field, field_validator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InteractiveType(Enum):
|
|
20
|
+
"""Supported interactive message types for WhatsApp."""
|
|
21
|
+
|
|
22
|
+
BUTTON = "button"
|
|
23
|
+
LIST = "list"
|
|
24
|
+
CTA_URL = "cta_url"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HeaderType(Enum):
|
|
28
|
+
"""Supported header types for interactive messages."""
|
|
29
|
+
|
|
30
|
+
TEXT = "text"
|
|
31
|
+
IMAGE = "image"
|
|
32
|
+
VIDEO = "video"
|
|
33
|
+
DOCUMENT = "document"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InteractiveMessage(BaseModel):
|
|
37
|
+
"""Base interactive message schema for interactive operations.
|
|
38
|
+
|
|
39
|
+
Common fields for all interactive message types based on existing
|
|
40
|
+
WhatsAppServiceInteractive implementation patterns.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
recipient: str = Field(
|
|
44
|
+
..., pattern=r"^\d{10,15}$", description="Recipient phone number"
|
|
45
|
+
)
|
|
46
|
+
body: str = Field(
|
|
47
|
+
..., min_length=1, max_length=4096, description="Main message text"
|
|
48
|
+
)
|
|
49
|
+
reply_to_message_id: str | None = Field(
|
|
50
|
+
None, description="Optional message ID for replies"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ReplyButton(BaseModel):
|
|
55
|
+
"""Reply button for button messages."""
|
|
56
|
+
|
|
57
|
+
id: str = Field(..., max_length=256, description="Unique button identifier")
|
|
58
|
+
title: str = Field(..., max_length=20, description="Button display text")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class InteractiveHeader(BaseModel):
|
|
62
|
+
"""Header for interactive messages with media support."""
|
|
63
|
+
|
|
64
|
+
type: HeaderType = Field(
|
|
65
|
+
..., description="Header type (text, image, video, document)"
|
|
66
|
+
)
|
|
67
|
+
text: str | None = Field(
|
|
68
|
+
None, max_length=60, description="Header text (for text headers)"
|
|
69
|
+
)
|
|
70
|
+
image: dict[str, str] | None = Field(
|
|
71
|
+
None, description="Image header with 'id' or 'link' key"
|
|
72
|
+
)
|
|
73
|
+
video: dict[str, str] | None = Field(
|
|
74
|
+
None, description="Video header with 'id' or 'link' key"
|
|
75
|
+
)
|
|
76
|
+
document: dict[str, str] | None = Field(
|
|
77
|
+
None, description="Document header with 'id' or 'link' key"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@field_validator("text")
|
|
81
|
+
@classmethod
|
|
82
|
+
def validate_text_header(cls, v, info):
|
|
83
|
+
"""Validate text header is provided for text type."""
|
|
84
|
+
if info.data and info.data.get("type") == HeaderType.TEXT and not v:
|
|
85
|
+
raise ValueError("Text header must include 'text' field")
|
|
86
|
+
return v
|
|
87
|
+
|
|
88
|
+
@field_validator("image", "video", "document")
|
|
89
|
+
@classmethod
|
|
90
|
+
def validate_media_header(cls, v, info):
|
|
91
|
+
"""Validate media headers have id or link."""
|
|
92
|
+
if v and not (v.get("id") or v.get("link")):
|
|
93
|
+
raise ValueError("Media header must include either 'id' or 'link'")
|
|
94
|
+
return v
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ButtonMessage(InteractiveMessage):
|
|
98
|
+
"""Button message schema for send_button_message operations.
|
|
99
|
+
|
|
100
|
+
Supports up to 3 quick reply buttons with text headers and footers.
|
|
101
|
+
Based on existing send_buttons_menu() implementation.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
interactive_type: Literal[InteractiveType.BUTTON] = Field(
|
|
105
|
+
default=InteractiveType.BUTTON
|
|
106
|
+
)
|
|
107
|
+
buttons: list[ReplyButton] = Field(
|
|
108
|
+
..., min_length=1, max_length=3, description="List of reply buttons (max 3)"
|
|
109
|
+
)
|
|
110
|
+
header: InteractiveHeader | None = Field(
|
|
111
|
+
None, description="Optional header with text/media content"
|
|
112
|
+
)
|
|
113
|
+
footer: str | None = Field(None, max_length=60, description="Optional footer text")
|
|
114
|
+
|
|
115
|
+
@field_validator("buttons")
|
|
116
|
+
@classmethod
|
|
117
|
+
def validate_button_uniqueness(cls, v):
|
|
118
|
+
"""Validate button IDs are unique."""
|
|
119
|
+
button_ids = [button.id for button in v]
|
|
120
|
+
if len(button_ids) != len(set(button_ids)):
|
|
121
|
+
raise ValueError("Button IDs must be unique")
|
|
122
|
+
return v
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ListRow(BaseModel):
|
|
126
|
+
"""Row within a list section."""
|
|
127
|
+
|
|
128
|
+
id: str = Field(..., max_length=200, description="Unique row identifier")
|
|
129
|
+
title: str = Field(..., max_length=24, description="Row title")
|
|
130
|
+
description: str | None = Field(
|
|
131
|
+
None, max_length=72, description="Optional row description"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ListSection(BaseModel):
|
|
136
|
+
"""Section within a list message."""
|
|
137
|
+
|
|
138
|
+
title: str = Field(..., max_length=24, description="Section title")
|
|
139
|
+
rows: list[ListRow] = Field(
|
|
140
|
+
...,
|
|
141
|
+
min_length=1,
|
|
142
|
+
max_length=10,
|
|
143
|
+
description="List of rows in this section (max 10)",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
@field_validator("rows")
|
|
147
|
+
@classmethod
|
|
148
|
+
def validate_row_uniqueness(cls, v):
|
|
149
|
+
"""Validate row IDs are unique within section."""
|
|
150
|
+
row_ids = [row.id for row in v]
|
|
151
|
+
if len(row_ids) != len(set(row_ids)):
|
|
152
|
+
raise ValueError("Row IDs must be unique within section")
|
|
153
|
+
return v
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ListMessage(InteractiveMessage):
|
|
157
|
+
"""List message schema for send_list_message operations.
|
|
158
|
+
|
|
159
|
+
Supports sectioned lists with up to 10 sections and 10 rows per section.
|
|
160
|
+
Based on existing send_list_menu() implementation.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
interactive_type: Literal[InteractiveType.LIST] = Field(
|
|
164
|
+
default=InteractiveType.LIST
|
|
165
|
+
)
|
|
166
|
+
button_text: str = Field(
|
|
167
|
+
..., max_length=20, description="Text for the button that opens the list"
|
|
168
|
+
)
|
|
169
|
+
sections: list[ListSection] = Field(
|
|
170
|
+
..., min_length=1, max_length=10, description="List of sections (max 10)"
|
|
171
|
+
)
|
|
172
|
+
header: str | None = Field(
|
|
173
|
+
None, max_length=60, description="Optional header text (text only for lists)"
|
|
174
|
+
)
|
|
175
|
+
footer: str | None = Field(None, max_length=60, description="Optional footer text")
|
|
176
|
+
|
|
177
|
+
@field_validator("sections")
|
|
178
|
+
@classmethod
|
|
179
|
+
def validate_global_row_uniqueness(cls, v):
|
|
180
|
+
"""Validate row IDs are unique across all sections."""
|
|
181
|
+
all_row_ids = []
|
|
182
|
+
for section in v:
|
|
183
|
+
for row in section.rows:
|
|
184
|
+
all_row_ids.append(row.id)
|
|
185
|
+
|
|
186
|
+
if len(all_row_ids) != len(set(all_row_ids)):
|
|
187
|
+
raise ValueError("Row IDs must be unique across all sections")
|
|
188
|
+
return v
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class CTAMessage(InteractiveMessage):
|
|
192
|
+
"""Call-to-Action message schema for send_cta_message operations.
|
|
193
|
+
|
|
194
|
+
Supports URL buttons for external links.
|
|
195
|
+
Based on existing send_cta_button() implementation.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
interactive_type: Literal[InteractiveType.CTA_URL] = Field(
|
|
199
|
+
default=InteractiveType.CTA_URL
|
|
200
|
+
)
|
|
201
|
+
button_text: str = Field(
|
|
202
|
+
..., min_length=1, description="Text to display on the button"
|
|
203
|
+
)
|
|
204
|
+
button_url: str = Field(
|
|
205
|
+
..., pattern=r"^https?://", description="URL to load when button is tapped"
|
|
206
|
+
)
|
|
207
|
+
header: str | None = Field(None, description="Optional header text")
|
|
208
|
+
footer: str | None = Field(None, description="Optional footer text")
|
|
209
|
+
|
|
210
|
+
@field_validator("button_url")
|
|
211
|
+
@classmethod
|
|
212
|
+
def validate_url_format(cls, v):
|
|
213
|
+
"""Validate URL format is http:// or https://."""
|
|
214
|
+
if not (v.startswith("http://") or v.startswith("https://")):
|
|
215
|
+
raise ValueError("button_url must start with http:// or https://")
|
|
216
|
+
return v
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class InteractiveResponse(BaseModel):
|
|
220
|
+
"""Response model for interactive message operations."""
|
|
221
|
+
|
|
222
|
+
success: bool = Field(..., description="Whether the operation succeeded")
|
|
223
|
+
message_id: str | None = Field(
|
|
224
|
+
None, description="WhatsApp message ID if successful"
|
|
225
|
+
)
|
|
226
|
+
interactive_type: InteractiveType = Field(
|
|
227
|
+
..., description="Type of interactive message sent"
|
|
228
|
+
)
|
|
229
|
+
recipient: str = Field(..., description="Recipient phone number")
|
|
230
|
+
error: str | None = Field(None, description="Error message if failed")
|
|
231
|
+
error_code: str | None = Field(None, description="Error code for specific failures")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# Validation utility functions for use in handlers
|
|
235
|
+
def validate_buttons_menu_limits(buttons: list[ReplyButton]) -> None:
|
|
236
|
+
"""Validate button menu constraints based on WhatsApp API limits."""
|
|
237
|
+
if len(buttons) > 3:
|
|
238
|
+
raise ValueError("Maximum of 3 buttons allowed")
|
|
239
|
+
|
|
240
|
+
for button in buttons:
|
|
241
|
+
if len(button.title) > 20:
|
|
242
|
+
raise ValueError(f"Button title '{button.title}' exceeds 20 characters")
|
|
243
|
+
if len(button.id) > 256:
|
|
244
|
+
raise ValueError(f"Button ID '{button.id}' exceeds 256 characters")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def validate_list_menu_limits(sections: list[ListSection]) -> None:
|
|
248
|
+
"""Validate list menu constraints based on WhatsApp API limits."""
|
|
249
|
+
if len(sections) > 10:
|
|
250
|
+
raise ValueError("Maximum of 10 sections allowed")
|
|
251
|
+
|
|
252
|
+
for section in sections:
|
|
253
|
+
if len(section.title) > 24:
|
|
254
|
+
raise ValueError(f"Section title '{section.title}' exceeds 24 characters")
|
|
255
|
+
|
|
256
|
+
if len(section.rows) > 10:
|
|
257
|
+
raise ValueError(f"Section '{section.title}' has more than 10 rows")
|
|
258
|
+
|
|
259
|
+
for row in section.rows:
|
|
260
|
+
if len(row.id) > 200:
|
|
261
|
+
raise ValueError(f"Row ID '{row.id}' exceeds 200 characters")
|
|
262
|
+
if len(row.title) > 24:
|
|
263
|
+
raise ValueError(f"Row title '{row.title}' exceeds 24 characters")
|
|
264
|
+
if row.description and len(row.description) > 72:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"Row description for '{row.title}' exceeds 72 characters"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def validate_header_constraints(
|
|
271
|
+
header: InteractiveHeader | None, footer: str | None
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Validate header and footer constraints."""
|
|
274
|
+
if header:
|
|
275
|
+
valid_header_types = {
|
|
276
|
+
HeaderType.TEXT,
|
|
277
|
+
HeaderType.IMAGE,
|
|
278
|
+
HeaderType.VIDEO,
|
|
279
|
+
HeaderType.DOCUMENT,
|
|
280
|
+
}
|
|
281
|
+
if header.type not in valid_header_types:
|
|
282
|
+
raise ValueError(
|
|
283
|
+
f"Header type must be one of {[t.value for t in valid_header_types]}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if footer and len(footer) > 60:
|
|
287
|
+
raise ValueError("Footer text cannot exceed 60 characters")
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Media message models for WhatsApp messaging.
|
|
3
|
+
|
|
4
|
+
Pydantic schemas for media messaging operations based on WhatsApp Cloud API 2025
|
|
5
|
+
specifications and existing handle_media.py implementation patterns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MediaType(Enum):
|
|
16
|
+
"""Supported media types for WhatsApp messages.
|
|
17
|
+
|
|
18
|
+
Based on WhatsApp Cloud API 2025 specifications and existing
|
|
19
|
+
WhatsAppServiceMedia.MediaType implementation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
AUDIO = "audio"
|
|
23
|
+
DOCUMENT = "document"
|
|
24
|
+
IMAGE = "image"
|
|
25
|
+
STICKER = "sticker"
|
|
26
|
+
VIDEO = "video"
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def get_supported_mime_types(cls, media_type: "MediaType") -> set[str]:
|
|
30
|
+
"""
|
|
31
|
+
Returns set of supported MIME types for each media type.
|
|
32
|
+
|
|
33
|
+
Extracted from existing WhatsAppServiceMedia.MediaType.get_supported_mime_types()
|
|
34
|
+
and validated against WhatsApp Cloud API 2025 specifications.
|
|
35
|
+
"""
|
|
36
|
+
SUPPORTED_TYPES = {
|
|
37
|
+
cls.AUDIO: {
|
|
38
|
+
"audio/aac",
|
|
39
|
+
"audio/mp4",
|
|
40
|
+
"audio/mpeg",
|
|
41
|
+
"audio/amr",
|
|
42
|
+
"audio/ogg",
|
|
43
|
+
},
|
|
44
|
+
cls.DOCUMENT: {
|
|
45
|
+
"text/plain",
|
|
46
|
+
"application/pdf",
|
|
47
|
+
"application/vnd.ms-powerpoint",
|
|
48
|
+
"application/msword",
|
|
49
|
+
"application/vnd.ms-excel",
|
|
50
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
51
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
52
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
53
|
+
},
|
|
54
|
+
cls.IMAGE: {"image/jpeg", "image/png"},
|
|
55
|
+
cls.STICKER: {"image/webp"},
|
|
56
|
+
cls.VIDEO: {"video/3gp", "video/mp4"},
|
|
57
|
+
}
|
|
58
|
+
return SUPPORTED_TYPES[media_type]
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def get_max_file_size(cls, media_type: "MediaType") -> int:
|
|
62
|
+
"""
|
|
63
|
+
Returns maximum file size in bytes for each media type.
|
|
64
|
+
|
|
65
|
+
Based on WhatsApp Cloud API 2025 specifications and existing
|
|
66
|
+
validation logic from handle_media.py.
|
|
67
|
+
"""
|
|
68
|
+
MAX_SIZES = {
|
|
69
|
+
cls.AUDIO: 16 * 1024 * 1024, # 16MB
|
|
70
|
+
cls.DOCUMENT: 100 * 1024 * 1024, # 100MB
|
|
71
|
+
cls.IMAGE: 5 * 1024 * 1024, # 5MB
|
|
72
|
+
cls.STICKER: 500 * 1024, # 500KB (animated), 100KB (static)
|
|
73
|
+
cls.VIDEO: 16 * 1024 * 1024, # 16MB
|
|
74
|
+
}
|
|
75
|
+
return MAX_SIZES[media_type]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MediaMessage(BaseModel):
|
|
79
|
+
"""Base media message schema for media operations.
|
|
80
|
+
|
|
81
|
+
Common fields for all media message types based on existing
|
|
82
|
+
send_media() method signature from handle_media.py.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
recipient: str = Field(
|
|
86
|
+
..., min_length=1, description="Recipient phone number or user identifier"
|
|
87
|
+
)
|
|
88
|
+
media_type: MediaType = Field(
|
|
89
|
+
..., description="Type of media (audio, document, image, sticker, video)"
|
|
90
|
+
)
|
|
91
|
+
media_source: str | Path = Field(
|
|
92
|
+
..., description="Either a URL string or a Path object to the local media file"
|
|
93
|
+
)
|
|
94
|
+
caption: str | None = Field(
|
|
95
|
+
None,
|
|
96
|
+
max_length=1024,
|
|
97
|
+
description="Optional caption for the media (not supported for audio and sticker)",
|
|
98
|
+
)
|
|
99
|
+
filename: str | None = Field(
|
|
100
|
+
None, description="Optional filename (used for documents)"
|
|
101
|
+
)
|
|
102
|
+
reply_to_message_id: str | None = Field(
|
|
103
|
+
None, description="Optional message ID for replies"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@field_validator("caption")
|
|
107
|
+
@classmethod
|
|
108
|
+
def validate_caption_for_media_type(cls, v, info):
|
|
109
|
+
"""Validate caption is not used for audio and sticker media types."""
|
|
110
|
+
if v is not None and info.data and "media_type" in info.data:
|
|
111
|
+
media_type = info.data["media_type"]
|
|
112
|
+
if media_type in (MediaType.AUDIO, MediaType.STICKER):
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Caption not supported for {media_type.value} media type"
|
|
115
|
+
)
|
|
116
|
+
return v
|
|
117
|
+
|
|
118
|
+
@field_validator("filename")
|
|
119
|
+
@classmethod
|
|
120
|
+
def validate_filename_for_documents(cls, v, info):
|
|
121
|
+
"""Validate filename is provided for document media types when needed."""
|
|
122
|
+
if v is not None and info.data and "media_type" in info.data:
|
|
123
|
+
media_type = info.data["media_type"]
|
|
124
|
+
if media_type != MediaType.DOCUMENT:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"Filename only supported for document media type, not {media_type.value}"
|
|
127
|
+
)
|
|
128
|
+
return v
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ImageMessage(MediaMessage):
|
|
132
|
+
"""Image message schema for send_image operations.
|
|
133
|
+
|
|
134
|
+
Supports JPEG and PNG images up to 5MB.
|
|
135
|
+
Images must be 8-bit, RGB or RGBA (WhatsApp Cloud API 2025).
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
media_type: Literal[MediaType.IMAGE] = Field(default=MediaType.IMAGE)
|
|
139
|
+
caption: str | None = Field(
|
|
140
|
+
None, max_length=1024, description="Optional caption for the image"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class VideoMessage(MediaMessage):
|
|
145
|
+
"""Video message schema for send_video operations.
|
|
146
|
+
|
|
147
|
+
Supports MP4 and 3GP videos up to 16MB.
|
|
148
|
+
Only H.264 video codec and AAC audio codec supported.
|
|
149
|
+
Single audio stream or no audio stream only (WhatsApp Cloud API 2025).
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
media_type: Literal[MediaType.VIDEO] = Field(default=MediaType.VIDEO)
|
|
153
|
+
caption: str | None = Field(
|
|
154
|
+
None, max_length=1024, description="Optional caption for the video"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class AudioMessage(MediaMessage):
|
|
159
|
+
"""Audio message schema for send_audio operations.
|
|
160
|
+
|
|
161
|
+
Supports AAC, AMR, MP3, M4A, and OGG audio up to 16MB.
|
|
162
|
+
OGG must use OPUS codecs only, mono input only (WhatsApp Cloud API 2025).
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
media_type: Literal[MediaType.AUDIO] = Field(default=MediaType.AUDIO)
|
|
166
|
+
caption: Literal[None] = Field(
|
|
167
|
+
default=None, description="Caption not supported for audio"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class DocumentMessage(MediaMessage):
|
|
172
|
+
"""Document message schema for send_document operations.
|
|
173
|
+
|
|
174
|
+
Supports TXT, PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX up to 100MB.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
media_type: Literal[MediaType.DOCUMENT] = Field(default=MediaType.DOCUMENT)
|
|
178
|
+
filename: str | None = Field(None, description="Optional filename for the document")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class StickerMessage(MediaMessage):
|
|
182
|
+
"""Sticker message schema for send_sticker operations.
|
|
183
|
+
|
|
184
|
+
Supports WebP images only.
|
|
185
|
+
Static stickers: 100KB max, Animated stickers: 500KB max.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
media_type: Literal[MediaType.STICKER] = Field(default=MediaType.STICKER)
|
|
189
|
+
caption: Literal[None] = Field(
|
|
190
|
+
default=None, description="Caption not supported for stickers"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class MediaUploadRequest(BaseModel):
|
|
195
|
+
"""Request schema for media upload operations.
|
|
196
|
+
|
|
197
|
+
Used for direct media upload endpoints.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
media_type: str = Field(..., description="MIME type of the media file")
|
|
201
|
+
filename: str | None = Field(None, description="Original filename of the media")
|
|
202
|
+
|
|
203
|
+
@field_validator("media_type")
|
|
204
|
+
@classmethod
|
|
205
|
+
def validate_mime_type(cls, v):
|
|
206
|
+
"""Validate MIME type is supported by WhatsApp."""
|
|
207
|
+
supported_types = set()
|
|
208
|
+
for media_type in MediaType:
|
|
209
|
+
supported_types.update(MediaType.get_supported_mime_types(media_type))
|
|
210
|
+
|
|
211
|
+
if v not in supported_types:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
f"Unsupported MIME type: {v}. Supported types: {sorted(supported_types)}"
|
|
214
|
+
)
|
|
215
|
+
return v
|