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,810 @@
1
+ """
2
+ WhatsApp webhook processor for the Mimeia AI Agent Platform.
3
+
4
+ This module provides comprehensive WhatsApp Business Platform webhook processing,
5
+ including message parsing, validation, and integration with the Symphony AI system.
6
+ """
7
+
8
+ import hashlib
9
+ import hmac
10
+ from datetime import datetime
11
+ from typing import Any
12
+
13
+ from pydantic import ValidationError
14
+
15
+ from wappa.core.config.settings import settings
16
+ from wappa.core.logging.context import set_request_context
17
+ from wappa.processors.base_processor import (
18
+ BaseWebhookProcessor,
19
+ # ProcessingResult removed - Universal Webhook Interface is the ONLY way
20
+ ProcessorCapabilities,
21
+ ProcessorError,
22
+ )
23
+ from wappa.schemas.core.types import ErrorCode, MessageType, PlatformType
24
+ from wappa.webhooks.core.base_message import BaseMessage
25
+ from wappa.webhooks.core.base_status import BaseMessageStatus
26
+ from wappa.webhooks.core.base_webhook import BaseWebhook
27
+
28
+
29
+ class WhatsAppWebhookProcessor(BaseWebhookProcessor):
30
+ """
31
+ WhatsApp Business Platform webhook processor.
32
+
33
+ Handles parsing, validation, and processing of WhatsApp webhooks
34
+ including incoming messages and outgoing message status updates.
35
+ Inherits from BaseWebhookProcessor for platform-agnostic interface.
36
+ """
37
+
38
+ def __init__(self):
39
+ """Initialize the WhatsApp processor with capabilities and handlers."""
40
+ super().__init__()
41
+
42
+ # Define WhatsApp-specific capabilities
43
+ self._capabilities = ProcessorCapabilities(
44
+ platform=PlatformType.WHATSAPP,
45
+ supported_message_types={
46
+ MessageType.TEXT,
47
+ MessageType.INTERACTIVE,
48
+ MessageType.IMAGE,
49
+ MessageType.AUDIO,
50
+ MessageType.VIDEO,
51
+ MessageType.DOCUMENT,
52
+ MessageType.CONTACT,
53
+ MessageType.LOCATION,
54
+ MessageType.STICKER,
55
+ MessageType.REACTION,
56
+ MessageType.SYSTEM,
57
+ # WhatsApp-specific types mapped to closest standard types
58
+ MessageType("button"), # Interactive button responses
59
+ MessageType("order"), # Product orders
60
+ MessageType("unsupported"), # Unsupported message types
61
+ },
62
+ supports_status_updates=True,
63
+ supports_signature_validation=True,
64
+ supports_error_webhooks=True,
65
+ max_payload_size=1024 * 1024, # 1MB typical WhatsApp webhook limit
66
+ rate_limit_per_minute=1000, # WhatsApp API rate limits
67
+ )
68
+
69
+ # Register message type handlers
70
+ self._register_message_handlers()
71
+
72
+ @property
73
+ def platform(self) -> PlatformType:
74
+ """Get the platform this processor handles."""
75
+ return PlatformType.WHATSAPP
76
+
77
+ @property
78
+ def capabilities(self) -> ProcessorCapabilities:
79
+ """Get the capabilities of this processor."""
80
+ return self._capabilities
81
+
82
+ def _register_message_handlers(self) -> None:
83
+ """Register handlers for all supported WhatsApp message types."""
84
+ self.register_message_handler("text", self._create_text_message)
85
+ self.register_message_handler("interactive", self._create_interactive_message)
86
+ self.register_message_handler("image", self._create_image_message)
87
+ self.register_message_handler("audio", self._create_audio_message)
88
+ self.register_message_handler("video", self._create_video_message)
89
+ self.register_message_handler("document", self._create_document_message)
90
+ self.register_message_handler("contact", self._create_contact_message)
91
+ self.register_message_handler("location", self._create_location_message)
92
+ self.register_message_handler("sticker", self._create_sticker_message)
93
+ self.register_message_handler("system", self._create_system_message)
94
+ self.register_message_handler("unsupported", self._create_unsupported_message)
95
+ self.register_message_handler("reaction", self._create_reaction_message)
96
+ self.register_message_handler("button", self._create_button_message)
97
+ self.register_message_handler("order", self._create_order_message)
98
+
99
+ # Legacy process_webhook method removed - Universal Webhook Interface is now the ONLY way
100
+ # Use create_universal_webhook() method instead for type-safe webhook handling
101
+
102
+ def validate_webhook_signature(
103
+ self, payload: bytes, signature: str, **kwargs
104
+ ) -> bool:
105
+ """
106
+ Validate WhatsApp webhook signature for security.
107
+
108
+ Args:
109
+ payload: Raw webhook payload bytes
110
+ signature: X-Hub-Signature-256 header from WhatsApp
111
+ **kwargs: Additional validation parameters
112
+
113
+ Returns:
114
+ True if signature is valid, False otherwise
115
+ """
116
+ if not settings.whatsapp_webhook_verify_token:
117
+ self.logger.warning(
118
+ "WhatsApp webhook verification token not configured - skipping signature validation"
119
+ )
120
+ return True
121
+
122
+ try:
123
+ # WhatsApp sends signature as 'sha256=<hash>'
124
+ if not signature.startswith("sha256="):
125
+ self.logger.error(
126
+ "Invalid signature format - must start with 'sha256='"
127
+ )
128
+ return False
129
+
130
+ # Extract the hash part
131
+ provided_hash = signature[7:] # Remove 'sha256=' prefix
132
+
133
+ # Calculate expected hash
134
+ expected_hash = hmac.new(
135
+ settings.whatsapp_webhook_verify_token.encode("utf-8"),
136
+ payload,
137
+ hashlib.sha256,
138
+ ).hexdigest()
139
+
140
+ # Compare hashes securely
141
+ is_valid = hmac.compare_digest(expected_hash, provided_hash)
142
+
143
+ if not is_valid:
144
+ self.logger.error("Webhook signature validation failed")
145
+
146
+ return is_valid
147
+
148
+ except Exception as e:
149
+ self.logger.error(f"Error validating webhook signature: {e}", exc_info=True)
150
+ return False
151
+
152
+ def parse_webhook_container(self, payload: dict[str, Any], **kwargs) -> BaseWebhook:
153
+ """
154
+ Parse the top-level WhatsApp webhook structure.
155
+
156
+ Args:
157
+ payload: Raw webhook payload
158
+ **kwargs: Additional parsing parameters
159
+
160
+ Returns:
161
+ Parsed webhook container with universal interface
162
+
163
+ Raises:
164
+ ValidationError: If webhook structure is invalid
165
+ """
166
+ try:
167
+ from wappa.webhooks.whatsapp.webhook_container import WhatsAppWebhook
168
+
169
+ webhook = WhatsAppWebhook.model_validate(payload)
170
+
171
+ self.logger.debug(
172
+ f"Successfully parsed WhatsApp webhook from {webhook.business_id}"
173
+ )
174
+ return webhook
175
+
176
+ except ValidationError as e:
177
+ error_msg = f"Failed to parse WhatsApp webhook structure: {e}"
178
+ self.logger.error(error_msg)
179
+ raise ValueError(error_msg) from e
180
+
181
+ def get_supported_message_types(self) -> set[MessageType]:
182
+ """Get the set of message types this processor supports."""
183
+ return self._capabilities.supported_message_types
184
+
185
+ def create_message_from_data(
186
+ self, message_data: dict[str, Any], message_type: MessageType, **kwargs
187
+ ) -> BaseMessage:
188
+ """
189
+ Create a message instance from raw data.
190
+
191
+ Args:
192
+ message_data: Raw message data from webhook
193
+ message_type: The type of message to create
194
+ **kwargs: Additional creation parameters
195
+
196
+ Returns:
197
+ Parsed message with universal interface
198
+
199
+ Raises:
200
+ ValidationError: If message data is invalid
201
+ UnsupportedMessageType: If message type is not supported
202
+ """
203
+ # Use the mapped universal message type for handler lookup instead of raw type
204
+ message_type_str = message_type.value
205
+
206
+ # Get appropriate handler
207
+ handler = self.get_message_handler(message_type_str)
208
+ if handler is None:
209
+ from .base_processor import UnsupportedMessageTypeError
210
+
211
+ raise UnsupportedMessageTypeError(message_type_str, self.platform)
212
+
213
+ # Create message instance
214
+ return handler(message_data, **kwargs)
215
+
216
+ def create_status_from_data(
217
+ self, status_data: dict[str, Any], **kwargs
218
+ ) -> BaseMessageStatus:
219
+ """
220
+ Create a status instance from raw data.
221
+
222
+ Args:
223
+ status_data: Raw status data from webhook
224
+ **kwargs: Additional creation parameters
225
+
226
+ Returns:
227
+ Parsed status with universal interface
228
+
229
+ Raises:
230
+ ValidationError: If status data is invalid
231
+ """
232
+ try:
233
+ from wappa.webhooks.whatsapp.status_models import WhatsAppMessageStatus
234
+
235
+ return WhatsAppMessageStatus.model_validate(status_data)
236
+
237
+ except ValidationError as e:
238
+ self.logger.error(f"Failed to parse WhatsApp message status: {e}")
239
+ raise
240
+
241
+ # Legacy _process_webhook_errors method removed - Universal Webhook Interface handles errors via ErrorWebhook
242
+
243
+ # Message creation handlers for all WhatsApp message types
244
+
245
+ def _create_text_message(
246
+ self, message_data: dict[str, Any], **kwargs
247
+ ) -> BaseMessage:
248
+ """Create a text message instance."""
249
+ from wappa.webhooks.whatsapp.message_types.text import WhatsAppTextMessage
250
+
251
+ return WhatsAppTextMessage.model_validate(message_data)
252
+
253
+ def _create_interactive_message(
254
+ self, message_data: dict[str, Any], **kwargs
255
+ ) -> BaseMessage:
256
+ """Create an interactive message instance."""
257
+ from wappa.webhooks.whatsapp.message_types.interactive import (
258
+ WhatsAppInteractiveMessage,
259
+ )
260
+
261
+ return WhatsAppInteractiveMessage.model_validate(message_data)
262
+
263
+ def _create_image_message(
264
+ self, message_data: dict[str, Any], **kwargs
265
+ ) -> BaseMessage:
266
+ """Create an image message instance."""
267
+ from wappa.webhooks.whatsapp.message_types.image import WhatsAppImageMessage
268
+
269
+ return WhatsAppImageMessage.model_validate(message_data)
270
+
271
+ def _create_audio_message(
272
+ self, message_data: dict[str, Any], **kwargs
273
+ ) -> BaseMessage:
274
+ """Create an audio message instance."""
275
+ from wappa.webhooks.whatsapp.message_types.audio import WhatsAppAudioMessage
276
+
277
+ return WhatsAppAudioMessage.model_validate(message_data)
278
+
279
+ def _create_video_message(
280
+ self, message_data: dict[str, Any], **kwargs
281
+ ) -> BaseMessage:
282
+ """Create a video message instance."""
283
+ from wappa.webhooks.whatsapp.message_types.video import WhatsAppVideoMessage
284
+
285
+ return WhatsAppVideoMessage.model_validate(message_data)
286
+
287
+ def _create_document_message(
288
+ self, message_data: dict[str, Any], **kwargs
289
+ ) -> BaseMessage:
290
+ """Create a document message instance."""
291
+ from wappa.webhooks.whatsapp.message_types.document import (
292
+ WhatsAppDocumentMessage,
293
+ )
294
+
295
+ return WhatsAppDocumentMessage.model_validate(message_data)
296
+
297
+ def _create_contact_message(
298
+ self, message_data: dict[str, Any], **kwargs
299
+ ) -> BaseMessage:
300
+ """Create a contact message instance."""
301
+ from wappa.webhooks.whatsapp.message_types.contact import WhatsAppContactMessage
302
+
303
+ return WhatsAppContactMessage.model_validate(message_data)
304
+
305
+ def _create_location_message(
306
+ self, message_data: dict[str, Any], **kwargs
307
+ ) -> BaseMessage:
308
+ """Create a location message instance."""
309
+ from wappa.webhooks.whatsapp.message_types.location import (
310
+ WhatsAppLocationMessage,
311
+ )
312
+
313
+ return WhatsAppLocationMessage.model_validate(message_data)
314
+
315
+ def _create_sticker_message(
316
+ self, message_data: dict[str, Any], **kwargs
317
+ ) -> BaseMessage:
318
+ """Create a sticker message instance."""
319
+ from wappa.webhooks.whatsapp.message_types.sticker import WhatsAppStickerMessage
320
+
321
+ return WhatsAppStickerMessage.model_validate(message_data)
322
+
323
+ def _create_system_message(
324
+ self, message_data: dict[str, Any], **kwargs
325
+ ) -> BaseMessage:
326
+ """Create a system message instance."""
327
+ from wappa.webhooks.whatsapp.message_types.system import WhatsAppSystemMessage
328
+
329
+ return WhatsAppSystemMessage.model_validate(message_data)
330
+
331
+ def _create_unsupported_message(
332
+ self, message_data: dict[str, Any], **kwargs
333
+ ) -> BaseMessage:
334
+ """Create an unsupported message instance."""
335
+ from wappa.webhooks.whatsapp.message_types.unsupported import (
336
+ WhatsAppUnsupportedMessage,
337
+ )
338
+
339
+ return WhatsAppUnsupportedMessage.model_validate(message_data)
340
+
341
+ def _create_reaction_message(
342
+ self, message_data: dict[str, Any], **kwargs
343
+ ) -> BaseMessage:
344
+ """Create a reaction message instance."""
345
+ from wappa.webhooks.whatsapp.message_types.reaction import (
346
+ WhatsAppReactionMessage,
347
+ )
348
+
349
+ return WhatsAppReactionMessage.model_validate(message_data)
350
+
351
+ def _create_button_message(
352
+ self, message_data: dict[str, Any], **kwargs
353
+ ) -> BaseMessage:
354
+ """Create a button message instance."""
355
+ from wappa.webhooks.whatsapp.message_types.button import WhatsAppButtonMessage
356
+
357
+ return WhatsAppButtonMessage.model_validate(message_data)
358
+
359
+ def _create_order_message(
360
+ self, message_data: dict[str, Any], **kwargs
361
+ ) -> BaseMessage:
362
+ """Create an order message instance."""
363
+ from wappa.webhooks.whatsapp.message_types.order import WhatsAppOrderMessage
364
+
365
+ return WhatsAppOrderMessage.model_validate(message_data)
366
+
367
+ # ===== Universal Webhook Interface Creation Methods =====
368
+
369
+ async def create_universal_webhook(
370
+ self, payload: dict[str, Any], tenant_id: str | None = None, **kwargs
371
+ ) -> "UniversalWebhook":
372
+ """
373
+ Transform WhatsApp webhook into Universal Webhook Interface.
374
+
375
+ This is the main adapter method that converts WhatsApp-specific webhook
376
+ payload into one of the 4 universal webhook types.
377
+
378
+ Args:
379
+ payload: Raw WhatsApp webhook payload
380
+ tenant_id: Tenant identifier for context
381
+ **kwargs: Additional processing parameters
382
+
383
+ Returns:
384
+ Universal webhook interface (IncomingMessageWebhook, StatusWebhook, or ErrorWebhook)
385
+
386
+ Raises:
387
+ ProcessorError: If webhook type cannot be determined or conversion fails
388
+ """
389
+
390
+ try:
391
+ # Parse webhook container first
392
+ webhook = self.parse_webhook_container(payload)
393
+
394
+ # Create tenant base from webhook metadata
395
+ tenant_base = self._create_tenant_base(webhook, tenant_id)
396
+
397
+ # Determine webhook type and create appropriate universal interface
398
+ if webhook.is_incoming_message:
399
+ universal_webhook = await self._create_incoming_message_webhook(
400
+ webhook, tenant_base, **kwargs
401
+ )
402
+ elif webhook.is_status_update:
403
+ universal_webhook = await self._create_status_webhook(
404
+ webhook, tenant_base, **kwargs
405
+ )
406
+ elif webhook.has_errors:
407
+ universal_webhook = await self._create_error_webhook(
408
+ webhook, tenant_base, **kwargs
409
+ )
410
+ else:
411
+ universal_webhook = None
412
+
413
+ # Set raw webhook data for debugging and inspection
414
+ if universal_webhook is not None:
415
+ universal_webhook.set_raw_webhook_data(payload)
416
+
417
+ # Set 3-context system: owner_id (URL), tenant_id (JSON), user_id (JSON)
418
+ webhook_tenant_id = tenant_base.platform_tenant_id # From JSON metadata
419
+
420
+ # Extract user_id based on webhook type
421
+ webhook_user_id = None
422
+ if hasattr(universal_webhook, "user") and universal_webhook.user:
423
+ # IncomingMessageWebhook has user object
424
+ webhook_user_id = universal_webhook.user.user_id
425
+ elif hasattr(universal_webhook, "recipient_id"):
426
+ # StatusWebhook has recipient_id field
427
+ webhook_user_id = universal_webhook.recipient_id
428
+ # ErrorWebhook has no user context (system-level errors)
429
+
430
+ # Set the context with webhook-extracted values
431
+ set_request_context(
432
+ tenant_id=webhook_tenant_id, # JSON tenant (authoritative)
433
+ user_id=webhook_user_id, # JSON user
434
+ # Note: owner_id is set by middleware from URL/settings
435
+ )
436
+
437
+ self.logger.debug(
438
+ f"✅ Set webhook context - tenant_id: {webhook_tenant_id}, user_id: {webhook_user_id}"
439
+ )
440
+
441
+ return universal_webhook
442
+
443
+ # Handle unknown webhook type
444
+ if universal_webhook is None:
445
+ # This could be an outgoing message webhook in the future
446
+ # For now, treat as error
447
+ from wappa.webhooks.core.webhook_interfaces import (
448
+ ErrorDetailBase,
449
+ ErrorWebhook,
450
+ )
451
+
452
+ error_detail = ErrorDetailBase(
453
+ error_code=400,
454
+ error_title="Unknown webhook type",
455
+ error_message="Webhook contains no recognizable content (messages, statuses, or errors)",
456
+ error_type="webhook_format",
457
+ occurred_at=datetime.utcnow(),
458
+ )
459
+
460
+ return ErrorWebhook(
461
+ tenant=tenant_base,
462
+ errors=[error_detail],
463
+ timestamp=datetime.utcnow(),
464
+ error_level="webhook",
465
+ platform=PlatformType.WHATSAPP,
466
+ webhook_id=webhook.get_webhook_id(),
467
+ )
468
+
469
+ except Exception as e:
470
+ self.logger.error(f"Failed to create universal webhook: {e}", exc_info=True)
471
+ raise ProcessorError(
472
+ f"Failed to transform WhatsApp webhook to universal interface: {e}",
473
+ ErrorCode.PROCESSING_ERROR,
474
+ PlatformType.WHATSAPP,
475
+ ) from e
476
+
477
+ def _create_tenant_base(
478
+ self, webhook: BaseWebhook, tenant_id: str | None = None
479
+ ) -> "TenantBase":
480
+ """
481
+ Create TenantBase from WhatsApp webhook metadata.
482
+
483
+ Args:
484
+ webhook: Parsed WhatsApp webhook container
485
+ tenant_id: Optional tenant identifier override
486
+
487
+ Returns:
488
+ TenantBase with business identification information
489
+ """
490
+ from wappa.webhooks.core.webhook_interfaces import TenantBase
491
+
492
+ # Extract metadata from WhatsApp webhook (metadata is wrapped, access underlying data)
493
+ metadata = webhook.get_metadata()
494
+
495
+ # Access the wrapped WhatsApp metadata
496
+ whatsapp_metadata = metadata._metadata
497
+
498
+ return TenantBase(
499
+ business_phone_number_id=whatsapp_metadata.phone_number_id,
500
+ display_phone_number=whatsapp_metadata.display_phone_number,
501
+ # For WhatsApp, the phone_number_id IS the tenant identifier
502
+ platform_tenant_id=whatsapp_metadata.phone_number_id,
503
+ )
504
+
505
+ async def _create_incoming_message_webhook(
506
+ self, webhook: BaseWebhook, tenant_base: "TenantBase", **kwargs
507
+ ) -> "IncomingMessageWebhook":
508
+ """
509
+ Create IncomingMessageWebhook from WhatsApp messages webhook.
510
+
511
+ Args:
512
+ webhook: Parsed WhatsApp webhook container
513
+ tenant_base: Tenant identification information
514
+ **kwargs: Additional processing parameters
515
+
516
+ Returns:
517
+ IncomingMessageWebhook with message and context information
518
+ """
519
+ from wappa.webhooks.core.webhook_interfaces import IncomingMessageWebhook
520
+
521
+ # Get the first message (WhatsApp typically sends one message per webhook)
522
+ raw_messages = webhook.get_raw_messages()
523
+ if not raw_messages:
524
+ raise ProcessorError(
525
+ "No messages found in incoming message webhook",
526
+ ErrorCode.PROCESSING_ERROR,
527
+ PlatformType.WHATSAPP,
528
+ )
529
+
530
+ # Parse the first message using the message type
531
+ raw_message = raw_messages[0]
532
+ raw_message_type = raw_message.get("type", "text")
533
+
534
+ # Map WhatsApp message types to universal message types
535
+ whatsapp_to_universal_type = {
536
+ "contacts": "contact", # WhatsApp uses 'contacts' but our enum uses 'contact'
537
+ # Add other mappings as needed
538
+ }
539
+ universal_message_type = whatsapp_to_universal_type.get(
540
+ raw_message_type, raw_message_type
541
+ )
542
+
543
+ message_type = MessageType(universal_message_type)
544
+ message = self.create_message_from_data(raw_message, message_type)
545
+
546
+ # Create user base from contacts
547
+ user_base = self._create_user_base_from_contacts(webhook, message.sender_id)
548
+
549
+ # Extract WhatsApp-specific contexts
550
+ business_context = self._extract_business_context(raw_message)
551
+ forward_context = self._extract_forward_context(raw_message)
552
+ ad_referral = self._extract_ad_referral(raw_message)
553
+
554
+ return IncomingMessageWebhook(
555
+ tenant=tenant_base,
556
+ user=user_base,
557
+ message=message,
558
+ business_context=business_context,
559
+ forward_context=forward_context,
560
+ ad_referral=ad_referral,
561
+ timestamp=datetime.fromtimestamp(message.timestamp),
562
+ platform=PlatformType.WHATSAPP,
563
+ webhook_id=webhook.get_webhook_id(),
564
+ )
565
+
566
+ async def _create_status_webhook(
567
+ self, webhook: BaseWebhook, tenant_base: "TenantBase", **kwargs
568
+ ) -> "StatusWebhook":
569
+ """
570
+ Create StatusWebhook from WhatsApp status webhook.
571
+
572
+ Args:
573
+ webhook: Parsed WhatsApp webhook container
574
+ tenant_base: Tenant identification information
575
+ **kwargs: Additional processing parameters
576
+
577
+ Returns:
578
+ StatusWebhook with message status information
579
+ """
580
+ from wappa.webhooks.core.webhook_interfaces import (
581
+ StatusWebhook,
582
+ )
583
+
584
+ # Get the first status (WhatsApp typically sends one status per webhook)
585
+ raw_statuses = webhook.get_raw_statuses()
586
+ if not raw_statuses:
587
+ raise ProcessorError(
588
+ "No statuses found in status webhook",
589
+ ErrorCode.PROCESSING_ERROR,
590
+ PlatformType.WHATSAPP,
591
+ )
592
+
593
+ # Parse the first status
594
+ raw_status = raw_statuses[0]
595
+ status = self.create_status_from_data(raw_status)
596
+
597
+ # Extract conversation and error context
598
+ conversation = self._extract_conversation_context(status)
599
+ errors = self._extract_status_errors(status)
600
+
601
+ return StatusWebhook(
602
+ tenant=tenant_base,
603
+ message_id=getattr(status, "message_id", ""),
604
+ status=getattr(status, "status", "unknown"),
605
+ recipient_id=getattr(status, "recipient_id", ""),
606
+ timestamp=datetime.fromtimestamp(getattr(status, "timestamp", 0)),
607
+ conversation=conversation,
608
+ errors=errors,
609
+ business_opaque_data=getattr(status, "business_opaque_data", None),
610
+ recipient_identity_hash=getattr(status, "recipient_identity_hash", None),
611
+ platform=PlatformType.WHATSAPP,
612
+ webhook_id=webhook.get_webhook_id(),
613
+ )
614
+
615
+ async def _create_error_webhook(
616
+ self, webhook: BaseWebhook, tenant_base: "TenantBase", **kwargs
617
+ ) -> "ErrorWebhook":
618
+ """
619
+ Create ErrorWebhook from WhatsApp error webhook.
620
+
621
+ Args:
622
+ webhook: Parsed WhatsApp webhook container
623
+ tenant_base: Tenant identification information
624
+ **kwargs: Additional processing parameters
625
+
626
+ Returns:
627
+ ErrorWebhook with error information
628
+ """
629
+ from wappa.webhooks.core.webhook_interfaces import ErrorDetailBase, ErrorWebhook
630
+
631
+ # Get errors from webhook (assuming the webhook has error data)
632
+ # For now, we'll extract from raw webhook data since there's no get_errors method
633
+ webhook_errors = []
634
+
635
+ # Convert to ErrorDetailBase list
636
+ error_details = []
637
+ for error in webhook_errors:
638
+ error_detail = ErrorDetailBase(
639
+ error_code=getattr(error, "code", 0),
640
+ error_title=getattr(error, "title", "Unknown error"),
641
+ error_message=getattr(error, "message", ""),
642
+ error_details=getattr(error, "details", None),
643
+ documentation_url=getattr(error, "href", None),
644
+ error_type="whatsapp_api",
645
+ occurred_at=datetime.utcnow(),
646
+ )
647
+ error_details.append(error_detail)
648
+
649
+ return ErrorWebhook(
650
+ tenant=tenant_base,
651
+ errors=error_details,
652
+ timestamp=datetime.utcnow(),
653
+ error_level="system",
654
+ platform=PlatformType.WHATSAPP,
655
+ webhook_id=webhook.get_webhook_id(),
656
+ )
657
+
658
+ def _create_user_base_from_contacts(
659
+ self, webhook: BaseWebhook, sender_id: str
660
+ ) -> "UserBase":
661
+ """
662
+ Create UserBase from WhatsApp contacts information.
663
+
664
+ Args:
665
+ webhook: Parsed WhatsApp webhook container
666
+ sender_id: Sender's user ID to match
667
+
668
+ Returns:
669
+ UserBase with user identification information
670
+ """
671
+ from wappa.webhooks.core.webhook_interfaces import UserBase
672
+
673
+ # Get contacts from webhook
674
+ contacts = webhook.get_contacts()
675
+
676
+ # Find matching contact
677
+ for contact in contacts:
678
+ if contact.user_id == sender_id:
679
+ return UserBase(
680
+ user_id=contact.user_id,
681
+ phone_number=contact.user_id, # For WhatsApp, user_id is the phone number
682
+ profile_name=contact.display_name,
683
+ identity_key_hash=getattr(contact, "identity_key_hash", None),
684
+ )
685
+
686
+ # Fallback if no matching contact found
687
+ return UserBase(
688
+ user_id=sender_id,
689
+ phone_number=sender_id,
690
+ profile_name=None,
691
+ identity_key_hash=None,
692
+ )
693
+
694
+ def _extract_business_context(
695
+ self, message_data: dict[str, Any]
696
+ ) -> "BusinessContextBase | None":
697
+ """Extract business context from WhatsApp message data."""
698
+ from wappa.webhooks.core.webhook_interfaces import BusinessContextBase
699
+
700
+ context = message_data.get("context")
701
+ if not context or not context.get("referred_product"):
702
+ return None
703
+
704
+ return BusinessContextBase(
705
+ contextual_message_id=context.get("id", ""),
706
+ business_phone_number=context.get("from", ""),
707
+ catalog_id=context["referred_product"].get("catalog_id"),
708
+ product_retailer_id=context["referred_product"].get("product_retailer_id"),
709
+ )
710
+
711
+ def _extract_forward_context(
712
+ self, message_data: dict[str, Any]
713
+ ) -> "ForwardContextBase | None":
714
+ """Extract forward context from WhatsApp message data."""
715
+ from wappa.webhooks.core.webhook_interfaces import ForwardContextBase
716
+
717
+ context = message_data.get("context")
718
+ if not context:
719
+ return None
720
+
721
+ is_forwarded = context.get("forwarded", False)
722
+ is_frequently_forwarded = context.get("frequently_forwarded", False)
723
+
724
+ if not (is_forwarded or is_frequently_forwarded):
725
+ return None
726
+
727
+ return ForwardContextBase(
728
+ is_forwarded=is_forwarded,
729
+ is_frequently_forwarded=is_frequently_forwarded,
730
+ forward_count=None, # WhatsApp doesn't provide exact count
731
+ original_sender=None, # WhatsApp doesn't provide original sender for privacy
732
+ )
733
+
734
+ def _extract_ad_referral(
735
+ self, message_data: dict[str, Any]
736
+ ) -> "AdReferralBase | None":
737
+ """Extract ad referral context from WhatsApp message data."""
738
+ from wappa.webhooks.core.webhook_interfaces import AdReferralBase
739
+
740
+ referral = message_data.get("referral")
741
+ if not referral:
742
+ return None
743
+
744
+ return AdReferralBase(
745
+ source_type=referral.get("source_type", "ad"),
746
+ source_id=referral.get("source_id", ""),
747
+ source_url=referral.get("source_url", ""),
748
+ ad_body=referral.get("body"),
749
+ ad_headline=referral.get("headline"),
750
+ media_type=referral.get("media_type"),
751
+ image_url=referral.get("image_url"),
752
+ video_url=referral.get("video_url"),
753
+ thumbnail_url=referral.get("thumbnail_url"),
754
+ click_id=referral.get("ctwa_clid"),
755
+ welcome_message_text=referral.get("welcome_message", {}).get("text"),
756
+ )
757
+
758
+ def _extract_conversation_context(
759
+ self, status_data: Any
760
+ ) -> "ConversationBase | None":
761
+ """Extract conversation context from WhatsApp status data."""
762
+ from wappa.webhooks.core.webhook_interfaces import ConversationBase
763
+
764
+ # Check if status has conversation data
765
+ if not hasattr(status_data, "conversation") or not status_data.conversation:
766
+ return None
767
+
768
+ conversation = status_data.conversation
769
+ pricing = getattr(status_data, "pricing", None)
770
+
771
+ return ConversationBase(
772
+ conversation_id=getattr(conversation, "id", ""),
773
+ expiration_timestamp=getattr(conversation, "expiration_timestamp", None),
774
+ category=getattr(conversation.origin, "type", None)
775
+ if hasattr(conversation, "origin")
776
+ else None,
777
+ origin_type=getattr(conversation.origin, "type", None)
778
+ if hasattr(conversation, "origin")
779
+ else None,
780
+ is_billable=getattr(pricing, "billable", None) if pricing else None,
781
+ pricing_model=getattr(pricing, "pricing_model", None) if pricing else None,
782
+ pricing_category=getattr(pricing, "category", None) if pricing else None,
783
+ pricing_type=getattr(pricing, "type", None) if pricing else None,
784
+ )
785
+
786
+ def _extract_status_errors(
787
+ self, status_data: Any
788
+ ) -> "list[ErrorDetailBase] | None":
789
+ """Extract error details from WhatsApp status data."""
790
+ from wappa.webhooks.core.webhook_interfaces import ErrorDetailBase
791
+
792
+ if not hasattr(status_data, "errors") or not status_data.errors:
793
+ return None
794
+
795
+ error_details = []
796
+ for error in status_data.errors:
797
+ error_detail = ErrorDetailBase(
798
+ error_code=getattr(error, "code", 0),
799
+ error_title=getattr(error, "title", "Unknown error"),
800
+ error_message=getattr(error, "message", ""),
801
+ error_details=getattr(error.error_data, "details", None)
802
+ if hasattr(error, "error_data") and error.error_data
803
+ else None,
804
+ documentation_url=getattr(error, "href", None),
805
+ error_type="delivery_failure",
806
+ occurred_at=datetime.utcnow(),
807
+ )
808
+ error_details.append(error_detail)
809
+
810
+ return error_details