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.

Files changed (211) hide show
  1. wappa/__init__.py +85 -0
  2. wappa/api/__init__.py +1 -0
  3. wappa/api/controllers/__init__.py +10 -0
  4. wappa/api/controllers/webhook_controller.py +441 -0
  5. wappa/api/dependencies/__init__.py +15 -0
  6. wappa/api/dependencies/whatsapp_dependencies.py +220 -0
  7. wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
  8. wappa/api/middleware/__init__.py +7 -0
  9. wappa/api/middleware/error_handler.py +158 -0
  10. wappa/api/middleware/owner.py +99 -0
  11. wappa/api/middleware/request_logging.py +184 -0
  12. wappa/api/routes/__init__.py +6 -0
  13. wappa/api/routes/health.py +102 -0
  14. wappa/api/routes/webhooks.py +211 -0
  15. wappa/api/routes/whatsapp/__init__.py +15 -0
  16. wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
  17. wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
  18. wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
  19. wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
  20. wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
  21. wappa/api/routes/whatsapp_combined.py +35 -0
  22. wappa/cli/__init__.py +9 -0
  23. wappa/cli/main.py +199 -0
  24. wappa/core/__init__.py +6 -0
  25. wappa/core/config/__init__.py +5 -0
  26. wappa/core/config/settings.py +161 -0
  27. wappa/core/events/__init__.py +41 -0
  28. wappa/core/events/default_handlers.py +642 -0
  29. wappa/core/events/event_dispatcher.py +244 -0
  30. wappa/core/events/event_handler.py +247 -0
  31. wappa/core/events/webhook_factory.py +219 -0
  32. wappa/core/factory/__init__.py +15 -0
  33. wappa/core/factory/plugin.py +68 -0
  34. wappa/core/factory/wappa_builder.py +326 -0
  35. wappa/core/logging/__init__.py +5 -0
  36. wappa/core/logging/context.py +100 -0
  37. wappa/core/logging/logger.py +343 -0
  38. wappa/core/plugins/__init__.py +34 -0
  39. wappa/core/plugins/auth_plugin.py +169 -0
  40. wappa/core/plugins/cors_plugin.py +128 -0
  41. wappa/core/plugins/custom_middleware_plugin.py +182 -0
  42. wappa/core/plugins/database_plugin.py +235 -0
  43. wappa/core/plugins/rate_limit_plugin.py +183 -0
  44. wappa/core/plugins/redis_plugin.py +224 -0
  45. wappa/core/plugins/wappa_core_plugin.py +261 -0
  46. wappa/core/plugins/webhook_plugin.py +253 -0
  47. wappa/core/types.py +108 -0
  48. wappa/core/wappa_app.py +546 -0
  49. wappa/database/__init__.py +18 -0
  50. wappa/database/adapter.py +107 -0
  51. wappa/database/adapters/__init__.py +17 -0
  52. wappa/database/adapters/mysql_adapter.py +187 -0
  53. wappa/database/adapters/postgresql_adapter.py +169 -0
  54. wappa/database/adapters/sqlite_adapter.py +174 -0
  55. wappa/domain/__init__.py +28 -0
  56. wappa/domain/builders/__init__.py +5 -0
  57. wappa/domain/builders/message_builder.py +189 -0
  58. wappa/domain/entities/__init__.py +5 -0
  59. wappa/domain/enums/messenger_platform.py +123 -0
  60. wappa/domain/factories/__init__.py +6 -0
  61. wappa/domain/factories/media_factory.py +450 -0
  62. wappa/domain/factories/message_factory.py +497 -0
  63. wappa/domain/factories/messenger_factory.py +244 -0
  64. wappa/domain/interfaces/__init__.py +32 -0
  65. wappa/domain/interfaces/base_repository.py +94 -0
  66. wappa/domain/interfaces/cache_factory.py +85 -0
  67. wappa/domain/interfaces/cache_interface.py +199 -0
  68. wappa/domain/interfaces/expiry_repository.py +68 -0
  69. wappa/domain/interfaces/media_interface.py +311 -0
  70. wappa/domain/interfaces/messaging_interface.py +523 -0
  71. wappa/domain/interfaces/pubsub_repository.py +151 -0
  72. wappa/domain/interfaces/repository_factory.py +108 -0
  73. wappa/domain/interfaces/shared_state_repository.py +122 -0
  74. wappa/domain/interfaces/state_repository.py +123 -0
  75. wappa/domain/interfaces/tables_repository.py +215 -0
  76. wappa/domain/interfaces/user_repository.py +114 -0
  77. wappa/domain/interfaces/webhooks/__init__.py +1 -0
  78. wappa/domain/models/media_result.py +110 -0
  79. wappa/domain/models/platforms/__init__.py +15 -0
  80. wappa/domain/models/platforms/platform_config.py +104 -0
  81. wappa/domain/services/__init__.py +11 -0
  82. wappa/domain/services/tenant_credentials_service.py +56 -0
  83. wappa/messaging/__init__.py +7 -0
  84. wappa/messaging/whatsapp/__init__.py +1 -0
  85. wappa/messaging/whatsapp/client/__init__.py +5 -0
  86. wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
  87. wappa/messaging/whatsapp/handlers/__init__.py +13 -0
  88. wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
  89. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
  90. wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
  91. wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
  92. wappa/messaging/whatsapp/messenger/__init__.py +5 -0
  93. wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
  94. wappa/messaging/whatsapp/models/__init__.py +61 -0
  95. wappa/messaging/whatsapp/models/basic_models.py +65 -0
  96. wappa/messaging/whatsapp/models/interactive_models.py +287 -0
  97. wappa/messaging/whatsapp/models/media_models.py +215 -0
  98. wappa/messaging/whatsapp/models/specialized_models.py +304 -0
  99. wappa/messaging/whatsapp/models/template_models.py +261 -0
  100. wappa/persistence/cache_factory.py +93 -0
  101. wappa/persistence/json/__init__.py +14 -0
  102. wappa/persistence/json/cache_adapters.py +271 -0
  103. wappa/persistence/json/handlers/__init__.py +1 -0
  104. wappa/persistence/json/handlers/state_handler.py +250 -0
  105. wappa/persistence/json/handlers/table_handler.py +263 -0
  106. wappa/persistence/json/handlers/user_handler.py +213 -0
  107. wappa/persistence/json/handlers/utils/__init__.py +1 -0
  108. wappa/persistence/json/handlers/utils/file_manager.py +153 -0
  109. wappa/persistence/json/handlers/utils/key_factory.py +11 -0
  110. wappa/persistence/json/handlers/utils/serialization.py +121 -0
  111. wappa/persistence/json/json_cache_factory.py +76 -0
  112. wappa/persistence/json/storage_manager.py +285 -0
  113. wappa/persistence/memory/__init__.py +14 -0
  114. wappa/persistence/memory/cache_adapters.py +271 -0
  115. wappa/persistence/memory/handlers/__init__.py +1 -0
  116. wappa/persistence/memory/handlers/state_handler.py +250 -0
  117. wappa/persistence/memory/handlers/table_handler.py +280 -0
  118. wappa/persistence/memory/handlers/user_handler.py +213 -0
  119. wappa/persistence/memory/handlers/utils/__init__.py +1 -0
  120. wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
  121. wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
  122. wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
  123. wappa/persistence/memory/memory_cache_factory.py +76 -0
  124. wappa/persistence/memory/storage_manager.py +235 -0
  125. wappa/persistence/redis/README.md +699 -0
  126. wappa/persistence/redis/__init__.py +11 -0
  127. wappa/persistence/redis/cache_adapters.py +285 -0
  128. wappa/persistence/redis/ops.py +880 -0
  129. wappa/persistence/redis/redis_cache_factory.py +71 -0
  130. wappa/persistence/redis/redis_client.py +231 -0
  131. wappa/persistence/redis/redis_handler/__init__.py +26 -0
  132. wappa/persistence/redis/redis_handler/state_handler.py +176 -0
  133. wappa/persistence/redis/redis_handler/table.py +158 -0
  134. wappa/persistence/redis/redis_handler/user.py +138 -0
  135. wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
  136. wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
  137. wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
  138. wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
  139. wappa/persistence/redis/redis_manager.py +189 -0
  140. wappa/processors/__init__.py +6 -0
  141. wappa/processors/base_processor.py +262 -0
  142. wappa/processors/factory.py +550 -0
  143. wappa/processors/whatsapp_processor.py +810 -0
  144. wappa/schemas/__init__.py +6 -0
  145. wappa/schemas/core/__init__.py +71 -0
  146. wappa/schemas/core/base_message.py +499 -0
  147. wappa/schemas/core/base_status.py +322 -0
  148. wappa/schemas/core/base_webhook.py +312 -0
  149. wappa/schemas/core/types.py +253 -0
  150. wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
  151. wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
  152. wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
  153. wappa/schemas/factory.py +754 -0
  154. wappa/schemas/webhooks/__init__.py +3 -0
  155. wappa/schemas/whatsapp/__init__.py +6 -0
  156. wappa/schemas/whatsapp/base_models.py +285 -0
  157. wappa/schemas/whatsapp/message_types/__init__.py +93 -0
  158. wappa/schemas/whatsapp/message_types/audio.py +350 -0
  159. wappa/schemas/whatsapp/message_types/button.py +267 -0
  160. wappa/schemas/whatsapp/message_types/contact.py +464 -0
  161. wappa/schemas/whatsapp/message_types/document.py +421 -0
  162. wappa/schemas/whatsapp/message_types/errors.py +195 -0
  163. wappa/schemas/whatsapp/message_types/image.py +424 -0
  164. wappa/schemas/whatsapp/message_types/interactive.py +430 -0
  165. wappa/schemas/whatsapp/message_types/location.py +416 -0
  166. wappa/schemas/whatsapp/message_types/order.py +372 -0
  167. wappa/schemas/whatsapp/message_types/reaction.py +271 -0
  168. wappa/schemas/whatsapp/message_types/sticker.py +328 -0
  169. wappa/schemas/whatsapp/message_types/system.py +317 -0
  170. wappa/schemas/whatsapp/message_types/text.py +411 -0
  171. wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
  172. wappa/schemas/whatsapp/message_types/video.py +344 -0
  173. wappa/schemas/whatsapp/status_models.py +479 -0
  174. wappa/schemas/whatsapp/validators.py +454 -0
  175. wappa/schemas/whatsapp/webhook_container.py +438 -0
  176. wappa/webhooks/__init__.py +17 -0
  177. wappa/webhooks/core/__init__.py +71 -0
  178. wappa/webhooks/core/base_message.py +499 -0
  179. wappa/webhooks/core/base_status.py +322 -0
  180. wappa/webhooks/core/base_webhook.py +312 -0
  181. wappa/webhooks/core/types.py +253 -0
  182. wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
  183. wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
  184. wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
  185. wappa/webhooks/factory.py +754 -0
  186. wappa/webhooks/whatsapp/__init__.py +6 -0
  187. wappa/webhooks/whatsapp/base_models.py +285 -0
  188. wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
  189. wappa/webhooks/whatsapp/message_types/audio.py +350 -0
  190. wappa/webhooks/whatsapp/message_types/button.py +267 -0
  191. wappa/webhooks/whatsapp/message_types/contact.py +464 -0
  192. wappa/webhooks/whatsapp/message_types/document.py +421 -0
  193. wappa/webhooks/whatsapp/message_types/errors.py +195 -0
  194. wappa/webhooks/whatsapp/message_types/image.py +424 -0
  195. wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
  196. wappa/webhooks/whatsapp/message_types/location.py +416 -0
  197. wappa/webhooks/whatsapp/message_types/order.py +372 -0
  198. wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
  199. wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
  200. wappa/webhooks/whatsapp/message_types/system.py +317 -0
  201. wappa/webhooks/whatsapp/message_types/text.py +411 -0
  202. wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
  203. wappa/webhooks/whatsapp/message_types/video.py +344 -0
  204. wappa/webhooks/whatsapp/status_models.py +479 -0
  205. wappa/webhooks/whatsapp/validators.py +454 -0
  206. wappa/webhooks/whatsapp/webhook_container.py +438 -0
  207. wappa-0.1.0.dist-info/METADATA +269 -0
  208. wappa-0.1.0.dist-info/RECORD +211 -0
  209. wappa-0.1.0.dist-info/WHEEL +4 -0
  210. wappa-0.1.0.dist-info/entry_points.txt +2 -0
  211. 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)