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,438 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main webhook container models for WhatsApp Business Platform.
|
|
3
|
+
|
|
4
|
+
This module contains the top-level webhook structure models that wrap
|
|
5
|
+
all WhatsApp message types and status updates.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
from wappa.webhooks.core.base_webhook import (
|
|
13
|
+
BaseContact,
|
|
14
|
+
BaseWebhook,
|
|
15
|
+
BaseWebhookMetadata,
|
|
16
|
+
)
|
|
17
|
+
from wappa.webhooks.core.types import PlatformType, WebhookType
|
|
18
|
+
from wappa.webhooks.whatsapp.base_models import WhatsAppContact, WhatsAppMetadata
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WebhookValue(BaseModel):
|
|
22
|
+
"""
|
|
23
|
+
The core value object containing webhook payload data.
|
|
24
|
+
|
|
25
|
+
This is where the actual message or status information is contained.
|
|
26
|
+
Either 'messages' OR 'statuses' will be present, never both.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
30
|
+
|
|
31
|
+
messaging_product: Literal["whatsapp"] = Field(
|
|
32
|
+
..., description="Always 'whatsapp' for WhatsApp Business webhooks"
|
|
33
|
+
)
|
|
34
|
+
metadata: WhatsAppMetadata = Field(
|
|
35
|
+
..., description="Business phone number metadata"
|
|
36
|
+
)
|
|
37
|
+
contacts: list[WhatsAppContact] | None = Field(
|
|
38
|
+
None, description="Contact information (present for incoming messages)"
|
|
39
|
+
)
|
|
40
|
+
messages: list[dict[str, Any]] | None = Field(
|
|
41
|
+
None,
|
|
42
|
+
description="Incoming messages array (parsed by specific message type schemas)",
|
|
43
|
+
)
|
|
44
|
+
statuses: list[dict[str, Any]] | None = Field(
|
|
45
|
+
None, description="Outgoing message status array (parsed by status schemas)"
|
|
46
|
+
)
|
|
47
|
+
errors: list[dict[str, Any]] | None = Field(
|
|
48
|
+
None, description="System, app, or account level errors"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@model_validator(mode="after")
|
|
52
|
+
def validate_webhook_content(self):
|
|
53
|
+
"""Ensure webhook has either messages, statuses, or errors."""
|
|
54
|
+
has_messages = self.messages is not None and len(self.messages) > 0
|
|
55
|
+
has_statuses = self.statuses is not None and len(self.statuses) > 0
|
|
56
|
+
has_errors = self.errors is not None and len(self.errors) > 0
|
|
57
|
+
|
|
58
|
+
if not (has_messages or has_statuses or has_errors):
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"Webhook must contain either messages, statuses, or errors"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Messages and statuses should not be present together
|
|
64
|
+
if has_messages and has_statuses:
|
|
65
|
+
raise ValueError("Webhook cannot contain both messages and statuses")
|
|
66
|
+
|
|
67
|
+
# If we have messages, we should have contacts
|
|
68
|
+
if has_messages and (self.contacts is None or len(self.contacts) == 0):
|
|
69
|
+
raise ValueError("Incoming messages must include contact information")
|
|
70
|
+
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
@field_validator("messages", "statuses", "errors")
|
|
74
|
+
@classmethod
|
|
75
|
+
def validate_arrays_not_empty(cls, v: list[dict] | None) -> list[dict] | None:
|
|
76
|
+
"""Validate that arrays are not empty if present."""
|
|
77
|
+
if v is not None and len(v) == 0:
|
|
78
|
+
return None # Convert empty arrays to None for cleaner logic
|
|
79
|
+
return v
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class WebhookChange(BaseModel):
|
|
83
|
+
"""
|
|
84
|
+
Change object describing what changed in the webhook.
|
|
85
|
+
|
|
86
|
+
For WhatsApp Business webhooks, the field is always 'messages'.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
90
|
+
|
|
91
|
+
field: Literal["messages"] = Field(
|
|
92
|
+
..., description="Always 'messages' for WhatsApp Business webhooks"
|
|
93
|
+
)
|
|
94
|
+
value: WebhookValue = Field(..., description="The webhook payload data")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class WebhookEntry(BaseModel):
|
|
98
|
+
"""
|
|
99
|
+
Entry object for WhatsApp Business Account webhook.
|
|
100
|
+
|
|
101
|
+
Contains the business account ID and the changes that occurred.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
105
|
+
|
|
106
|
+
id: str = Field(..., description="WhatsApp Business Account ID")
|
|
107
|
+
changes: list[WebhookChange] = Field(
|
|
108
|
+
..., description="Array of changes (typically contains one change)"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@field_validator("id")
|
|
112
|
+
@classmethod
|
|
113
|
+
def validate_business_account_id(cls, v: str) -> str:
|
|
114
|
+
"""Validate business account ID format."""
|
|
115
|
+
if not v or not v.isdigit():
|
|
116
|
+
raise ValueError("Business account ID must be numeric")
|
|
117
|
+
if len(v) < 10:
|
|
118
|
+
raise ValueError("Business account ID must be at least 10 digits")
|
|
119
|
+
return v
|
|
120
|
+
|
|
121
|
+
@field_validator("changes")
|
|
122
|
+
@classmethod
|
|
123
|
+
def validate_changes_not_empty(cls, v: list[WebhookChange]) -> list[WebhookChange]:
|
|
124
|
+
"""Validate changes array is not empty."""
|
|
125
|
+
if not v or len(v) == 0:
|
|
126
|
+
raise ValueError("Changes array cannot be empty")
|
|
127
|
+
return v
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class WhatsAppWebhookMetadata(BaseWebhookMetadata):
|
|
131
|
+
"""
|
|
132
|
+
WhatsApp-specific webhook metadata implementation.
|
|
133
|
+
|
|
134
|
+
Wraps WhatsApp metadata to provide universal interface.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, whatsapp_metadata: WhatsAppMetadata):
|
|
138
|
+
super().__init__()
|
|
139
|
+
self._metadata = whatsapp_metadata
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def business_id(self) -> str:
|
|
143
|
+
"""Get the business phone number ID."""
|
|
144
|
+
return self._metadata.phone_number_id
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def webhook_source_id(self) -> str:
|
|
148
|
+
"""Get the webhook source identifier (phone number ID)."""
|
|
149
|
+
return self._metadata.phone_number_id
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def platform(self) -> PlatformType:
|
|
153
|
+
"""Always WhatsApp for this implementation."""
|
|
154
|
+
return PlatformType.WHATSAPP
|
|
155
|
+
|
|
156
|
+
def to_universal_dict(self) -> dict[str, Any]:
|
|
157
|
+
"""Convert to platform-agnostic dictionary representation."""
|
|
158
|
+
return {
|
|
159
|
+
"platform": self.platform.value,
|
|
160
|
+
"business_id": self.business_id,
|
|
161
|
+
"webhook_source_id": self.webhook_source_id,
|
|
162
|
+
"display_phone_number": self._metadata.display_phone_number,
|
|
163
|
+
"whatsapp_data": {
|
|
164
|
+
"phone_number_id": self._metadata.phone_number_id,
|
|
165
|
+
"display_phone_number": self._metadata.display_phone_number,
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class WhatsAppContactAdapter(BaseContact):
|
|
171
|
+
"""
|
|
172
|
+
WhatsApp-specific contact adapter for universal interface.
|
|
173
|
+
|
|
174
|
+
Adapts WhatsApp contact data to the universal contact interface.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(self, whatsapp_contact: WhatsAppContact):
|
|
178
|
+
super().__init__()
|
|
179
|
+
self._contact = whatsapp_contact
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def user_id(self) -> str:
|
|
183
|
+
"""Get the universal user identifier (WhatsApp ID)."""
|
|
184
|
+
return self._contact.wa_id
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def display_name(self) -> str | None:
|
|
188
|
+
"""Get the user's display name (profile name)."""
|
|
189
|
+
return self._contact.profile.name if self._contact.profile else None
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def platform(self) -> PlatformType:
|
|
193
|
+
"""Always WhatsApp for this implementation."""
|
|
194
|
+
return PlatformType.WHATSAPP
|
|
195
|
+
|
|
196
|
+
def to_universal_dict(self) -> dict[str, Any]:
|
|
197
|
+
"""Convert to platform-agnostic dictionary representation."""
|
|
198
|
+
return {
|
|
199
|
+
"platform": self.platform.value,
|
|
200
|
+
"user_id": self.user_id,
|
|
201
|
+
"display_name": self.display_name,
|
|
202
|
+
"whatsapp_data": {
|
|
203
|
+
"wa_id": self._contact.wa_id,
|
|
204
|
+
"profile": self._contact.profile.model_dump()
|
|
205
|
+
if self._contact.profile
|
|
206
|
+
else None,
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class WhatsAppWebhook(BaseWebhook):
|
|
212
|
+
"""
|
|
213
|
+
Top-level WhatsApp Business Platform webhook model.
|
|
214
|
+
|
|
215
|
+
This is the root model for all WhatsApp webhook payloads.
|
|
216
|
+
Use this model to parse incoming webhook requests.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
model_config = ConfigDict(
|
|
220
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
object: Literal["whatsapp_business_account"] = Field(
|
|
224
|
+
..., description="Always 'whatsapp_business_account' for WhatsApp webhooks"
|
|
225
|
+
)
|
|
226
|
+
entry: list[WebhookEntry] = Field(
|
|
227
|
+
..., description="Array of webhook entries (typically contains one entry)"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
@field_validator("entry")
|
|
231
|
+
@classmethod
|
|
232
|
+
def validate_entry_not_empty(cls, v: list[WebhookEntry]) -> list[WebhookEntry]:
|
|
233
|
+
"""Validate entry array is not empty."""
|
|
234
|
+
if not v or len(v) == 0:
|
|
235
|
+
raise ValueError("Entry array cannot be empty")
|
|
236
|
+
return v
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def is_incoming_message(self) -> bool:
|
|
240
|
+
"""Check if this webhook contains incoming messages."""
|
|
241
|
+
if not self.entry:
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
for entry in self.entry:
|
|
245
|
+
for change in entry.changes:
|
|
246
|
+
if change.value.messages is not None:
|
|
247
|
+
return True
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def is_status_update(self) -> bool:
|
|
252
|
+
"""Check if this webhook contains message status updates."""
|
|
253
|
+
if not self.entry:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
for entry in self.entry:
|
|
257
|
+
for change in entry.changes:
|
|
258
|
+
if change.value.statuses is not None:
|
|
259
|
+
return True
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def has_errors(self) -> bool:
|
|
264
|
+
"""Check if this webhook contains errors."""
|
|
265
|
+
if not self.entry:
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
for entry in self.entry:
|
|
269
|
+
for change in entry.changes:
|
|
270
|
+
if change.value.errors is not None:
|
|
271
|
+
return True
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
def get_business_account_id(self) -> str:
|
|
275
|
+
"""Get the WhatsApp Business Account ID from the first entry."""
|
|
276
|
+
if not self.entry:
|
|
277
|
+
raise ValueError("No entry data available")
|
|
278
|
+
return self.entry[0].id
|
|
279
|
+
|
|
280
|
+
def get_phone_number_id(self) -> str:
|
|
281
|
+
"""Get the business phone number ID from the first entry."""
|
|
282
|
+
if not self.entry:
|
|
283
|
+
raise ValueError("No entry data available")
|
|
284
|
+
return self.entry[0].changes[0].value.metadata.phone_number_id
|
|
285
|
+
|
|
286
|
+
def get_display_phone_number(self) -> str:
|
|
287
|
+
"""Get the business display phone number from the first entry."""
|
|
288
|
+
if not self.entry:
|
|
289
|
+
raise ValueError("No entry data available")
|
|
290
|
+
return self.entry[0].changes[0].value.metadata.display_phone_number
|
|
291
|
+
|
|
292
|
+
def get_raw_messages(self) -> list[dict[str, Any]]:
|
|
293
|
+
"""
|
|
294
|
+
Get raw message data for parsing with specific message type schemas.
|
|
295
|
+
|
|
296
|
+
Returns empty list if no messages present.
|
|
297
|
+
"""
|
|
298
|
+
messages = []
|
|
299
|
+
for entry in self.entry:
|
|
300
|
+
for change in entry.changes:
|
|
301
|
+
if change.value.messages:
|
|
302
|
+
messages.extend(change.value.messages)
|
|
303
|
+
return messages
|
|
304
|
+
|
|
305
|
+
def get_raw_statuses(self) -> list[dict[str, Any]]:
|
|
306
|
+
"""
|
|
307
|
+
Get raw status data for parsing with status schemas.
|
|
308
|
+
|
|
309
|
+
Returns empty list if no statuses present.
|
|
310
|
+
"""
|
|
311
|
+
statuses = []
|
|
312
|
+
for entry in self.entry:
|
|
313
|
+
for change in entry.changes:
|
|
314
|
+
if change.value.statuses:
|
|
315
|
+
statuses.extend(change.value.statuses)
|
|
316
|
+
return statuses
|
|
317
|
+
|
|
318
|
+
def get_contacts(self) -> list[BaseContact]:
|
|
319
|
+
"""
|
|
320
|
+
Get contact information from the webhook with universal interface.
|
|
321
|
+
|
|
322
|
+
Returns empty list if no contacts present.
|
|
323
|
+
"""
|
|
324
|
+
contacts = []
|
|
325
|
+
for entry in self.entry:
|
|
326
|
+
for change in entry.changes:
|
|
327
|
+
if change.value.contacts:
|
|
328
|
+
# Convert WhatsApp contacts to universal interface
|
|
329
|
+
adapted_contacts = [
|
|
330
|
+
WhatsAppContactAdapter(contact)
|
|
331
|
+
for contact in change.value.contacts
|
|
332
|
+
]
|
|
333
|
+
contacts.extend(adapted_contacts)
|
|
334
|
+
return contacts
|
|
335
|
+
|
|
336
|
+
def get_whatsapp_contacts(self) -> list[WhatsAppContact]:
|
|
337
|
+
"""
|
|
338
|
+
Get original WhatsApp contact objects (platform-specific).
|
|
339
|
+
|
|
340
|
+
Returns empty list if no contacts present.
|
|
341
|
+
"""
|
|
342
|
+
contacts = []
|
|
343
|
+
for entry in self.entry:
|
|
344
|
+
for change in entry.changes:
|
|
345
|
+
if change.value.contacts:
|
|
346
|
+
contacts.extend(change.value.contacts)
|
|
347
|
+
return contacts
|
|
348
|
+
|
|
349
|
+
# Implement abstract methods from BaseWebhook
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def platform(self) -> PlatformType:
|
|
353
|
+
"""Get the platform type this webhook came from."""
|
|
354
|
+
return PlatformType.WHATSAPP
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def webhook_type(self) -> WebhookType:
|
|
358
|
+
"""Get the type of webhook (messages, status updates, errors, etc.)."""
|
|
359
|
+
if self.is_incoming_message:
|
|
360
|
+
return WebhookType.INCOMING_MESSAGES
|
|
361
|
+
elif self.is_status_update:
|
|
362
|
+
return WebhookType.STATUS_UPDATES
|
|
363
|
+
elif self.has_errors:
|
|
364
|
+
return WebhookType.ERRORS
|
|
365
|
+
else:
|
|
366
|
+
return WebhookType.ERRORS # Default fallback
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def business_id(self) -> str:
|
|
370
|
+
"""Get the business/account identifier."""
|
|
371
|
+
return self.get_business_account_id()
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def source_id(self) -> str:
|
|
375
|
+
"""Get the webhook source identifier (phone number ID)."""
|
|
376
|
+
return self.get_phone_number_id()
|
|
377
|
+
|
|
378
|
+
def get_metadata(self) -> BaseWebhookMetadata:
|
|
379
|
+
"""Get webhook metadata with universal interface."""
|
|
380
|
+
if not self.entry:
|
|
381
|
+
raise ValueError("No entry data available")
|
|
382
|
+
|
|
383
|
+
whatsapp_metadata = self.entry[0].changes[0].value.metadata
|
|
384
|
+
return WhatsAppWebhookMetadata(whatsapp_metadata)
|
|
385
|
+
|
|
386
|
+
def to_universal_dict(self) -> dict[str, Any]:
|
|
387
|
+
"""Convert webhook to platform-agnostic dictionary representation."""
|
|
388
|
+
return {
|
|
389
|
+
"platform": self.platform.value,
|
|
390
|
+
"webhook_type": self.webhook_type.value,
|
|
391
|
+
"business_id": self.business_id,
|
|
392
|
+
"source_id": self.source_id,
|
|
393
|
+
"received_at": self.received_at.isoformat(),
|
|
394
|
+
"has_messages": self.is_incoming_message(),
|
|
395
|
+
"has_statuses": self.is_status_update(),
|
|
396
|
+
"has_errors": self.has_errors(),
|
|
397
|
+
"message_count": len(self.get_raw_messages()),
|
|
398
|
+
"status_count": len(self.get_raw_statuses()),
|
|
399
|
+
"contact_count": len(self.get_contacts()),
|
|
400
|
+
"metadata": self.get_metadata().to_universal_dict(),
|
|
401
|
+
"whatsapp_data": {
|
|
402
|
+
"object": self.object,
|
|
403
|
+
"business_account_id": self.business_id,
|
|
404
|
+
"phone_number_id": self.source_id,
|
|
405
|
+
"display_phone_number": self.get_display_phone_number(),
|
|
406
|
+
},
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
def get_processing_context(self) -> dict[str, Any]:
|
|
410
|
+
"""Get context information needed for message processing."""
|
|
411
|
+
return {
|
|
412
|
+
"platform": self.platform.value,
|
|
413
|
+
"business_id": self.business_id,
|
|
414
|
+
"source_id": self.source_id,
|
|
415
|
+
"webhook_type": self.webhook_type.value,
|
|
416
|
+
"display_phone_number": self.get_display_phone_number(),
|
|
417
|
+
"webhook_id": self.get_webhook_id(),
|
|
418
|
+
"received_at": self.received_at.isoformat(),
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
@classmethod
|
|
422
|
+
def from_platform_payload(
|
|
423
|
+
cls, payload: dict[str, Any], **kwargs
|
|
424
|
+
) -> "WhatsAppWebhook":
|
|
425
|
+
"""
|
|
426
|
+
Create webhook instance from WhatsApp-specific payload.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
payload: Raw webhook payload from WhatsApp
|
|
430
|
+
**kwargs: Additional WhatsApp-specific parameters
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Validated WhatsApp webhook instance
|
|
434
|
+
|
|
435
|
+
Raises:
|
|
436
|
+
ValidationError: If payload is invalid
|
|
437
|
+
"""
|
|
438
|
+
return cls.model_validate(payload)
|