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,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
|