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,653 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp interactive message handler implementation.
|
|
3
|
+
|
|
4
|
+
Provides interactive messaging functionality for WhatsApp Business API:
|
|
5
|
+
- Button messages (quick reply buttons, max 3)
|
|
6
|
+
- List messages (sectioned lists, max 10 sections with 10 rows each)
|
|
7
|
+
- Call-to-action messages (URL buttons)
|
|
8
|
+
|
|
9
|
+
This handler is used by WhatsAppMessenger via composition pattern to implement
|
|
10
|
+
the interactive methods of the IMessenger interface.
|
|
11
|
+
|
|
12
|
+
Based on existing whatsapp_latest/services/interactive_message.py functionality
|
|
13
|
+
with SOLID architecture improvements.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from wappa.core.logging.logger import get_logger
|
|
17
|
+
from wappa.messaging.whatsapp.client.whatsapp_client import WhatsAppClient
|
|
18
|
+
from wappa.messaging.whatsapp.models.basic_models import MessageResult
|
|
19
|
+
from wappa.messaging.whatsapp.models.interactive_models import (
|
|
20
|
+
HeaderType,
|
|
21
|
+
InteractiveHeader,
|
|
22
|
+
ReplyButton,
|
|
23
|
+
validate_buttons_menu_limits,
|
|
24
|
+
validate_header_constraints,
|
|
25
|
+
)
|
|
26
|
+
from wappa.schemas.core.types import PlatformType
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WhatsAppInteractiveHandler:
|
|
30
|
+
"""
|
|
31
|
+
WhatsApp-specific implementation for interactive messaging operations.
|
|
32
|
+
|
|
33
|
+
Provides methods for sending interactive messages via WhatsApp Business API:
|
|
34
|
+
- send_buttons_menu: Quick reply button messages
|
|
35
|
+
- send_list_menu: Sectioned list messages
|
|
36
|
+
- send_cta_button: Call-to-action URL button messages
|
|
37
|
+
|
|
38
|
+
Used by WhatsAppMessenger via composition to implement IMessenger interactive methods.
|
|
39
|
+
Follows the same patterns as WhatsAppMediaHandler for consistency.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, client: WhatsAppClient, tenant_id: str):
|
|
43
|
+
"""Initialize interactive handler with WhatsApp client.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
client: Configured WhatsApp client for API operations
|
|
47
|
+
tenant_id: Tenant identifier (phone_number_id in WhatsApp context)
|
|
48
|
+
"""
|
|
49
|
+
self.client = client
|
|
50
|
+
self._tenant_id = tenant_id
|
|
51
|
+
self.logger = get_logger(__name__)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def platform(self) -> PlatformType:
|
|
55
|
+
"""Get the platform this handler operates on."""
|
|
56
|
+
return PlatformType.WHATSAPP
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def tenant_id(self) -> str:
|
|
60
|
+
"""Get the tenant ID this handler serves."""
|
|
61
|
+
return self._tenant_id
|
|
62
|
+
|
|
63
|
+
async def send_buttons_menu(
|
|
64
|
+
self,
|
|
65
|
+
to: str,
|
|
66
|
+
body: str,
|
|
67
|
+
buttons: list[ReplyButton],
|
|
68
|
+
header: InteractiveHeader | None = None,
|
|
69
|
+
footer_text: str | None = None,
|
|
70
|
+
reply_to_message_id: str | None = None,
|
|
71
|
+
) -> MessageResult:
|
|
72
|
+
"""
|
|
73
|
+
Send an interactive button menu message via WhatsApp.
|
|
74
|
+
|
|
75
|
+
Based on existing WhatsAppServiceInteractive.send_buttons_menu() with
|
|
76
|
+
improved error handling, logging, and result structure.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
to: Recipient's phone number
|
|
80
|
+
body: Main message text (max 1024 chars)
|
|
81
|
+
buttons: List of ReplyButton models (max 3 buttons)
|
|
82
|
+
header: Optional InteractiveHeader model with type and content
|
|
83
|
+
footer_text: Footer text (max 60 chars)
|
|
84
|
+
reply_to_message_id: Optional message ID to reply to
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
MessageResult with operation status and metadata
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If any input parameters are invalid
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
# Validate input parameters using utility functions
|
|
94
|
+
if len(body) > 1024:
|
|
95
|
+
return MessageResult(
|
|
96
|
+
success=False,
|
|
97
|
+
error="Body text cannot exceed 1024 characters",
|
|
98
|
+
error_code="BODY_TOO_LONG",
|
|
99
|
+
recipient=to,
|
|
100
|
+
platform=PlatformType.WHATSAPP,
|
|
101
|
+
tenant_id=self._tenant_id,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
validate_buttons_menu_limits(buttons)
|
|
105
|
+
if header:
|
|
106
|
+
validate_header_constraints(header, footer_text)
|
|
107
|
+
|
|
108
|
+
# Validate footer length
|
|
109
|
+
if footer_text and len(footer_text) > 60:
|
|
110
|
+
return MessageResult(
|
|
111
|
+
success=False,
|
|
112
|
+
error="Footer text cannot exceed 60 characters",
|
|
113
|
+
error_code="FOOTER_TOO_LONG",
|
|
114
|
+
recipient=to,
|
|
115
|
+
platform=PlatformType.WHATSAPP,
|
|
116
|
+
tenant_id=self._tenant_id,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Validate header if provided - adapted for InteractiveHeader model
|
|
120
|
+
if header:
|
|
121
|
+
valid_header_types = {
|
|
122
|
+
HeaderType.TEXT,
|
|
123
|
+
HeaderType.IMAGE,
|
|
124
|
+
HeaderType.VIDEO,
|
|
125
|
+
HeaderType.DOCUMENT,
|
|
126
|
+
}
|
|
127
|
+
if header.type not in valid_header_types:
|
|
128
|
+
return MessageResult(
|
|
129
|
+
success=False,
|
|
130
|
+
error=f"Header type must be one of {[t.value for t in valid_header_types]}",
|
|
131
|
+
error_code="INVALID_HEADER_TYPE",
|
|
132
|
+
recipient=to,
|
|
133
|
+
platform=PlatformType.WHATSAPP,
|
|
134
|
+
tenant_id=self._tenant_id,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Validate text header
|
|
138
|
+
if header.type == HeaderType.TEXT and not header.text:
|
|
139
|
+
return MessageResult(
|
|
140
|
+
success=False,
|
|
141
|
+
error="Text header must include 'text' field",
|
|
142
|
+
error_code="INVALID_TEXT_HEADER",
|
|
143
|
+
recipient=to,
|
|
144
|
+
platform=PlatformType.WHATSAPP,
|
|
145
|
+
tenant_id=self._tenant_id,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Validate media headers
|
|
149
|
+
if header.type == HeaderType.IMAGE:
|
|
150
|
+
if not header.image or (
|
|
151
|
+
not header.image.get("id") and not header.image.get("link")
|
|
152
|
+
):
|
|
153
|
+
return MessageResult(
|
|
154
|
+
success=False,
|
|
155
|
+
error="Image header must include either 'id' or 'link'",
|
|
156
|
+
error_code="INVALID_MEDIA_HEADER",
|
|
157
|
+
recipient=to,
|
|
158
|
+
platform=PlatformType.WHATSAPP,
|
|
159
|
+
tenant_id=self._tenant_id,
|
|
160
|
+
)
|
|
161
|
+
elif header.type == HeaderType.VIDEO:
|
|
162
|
+
if not header.video or (
|
|
163
|
+
not header.video.get("id") and not header.video.get("link")
|
|
164
|
+
):
|
|
165
|
+
return MessageResult(
|
|
166
|
+
success=False,
|
|
167
|
+
error="Video header must include either 'id' or 'link'",
|
|
168
|
+
error_code="INVALID_MEDIA_HEADER",
|
|
169
|
+
recipient=to,
|
|
170
|
+
platform=PlatformType.WHATSAPP,
|
|
171
|
+
tenant_id=self._tenant_id,
|
|
172
|
+
)
|
|
173
|
+
elif header.type == HeaderType.DOCUMENT:
|
|
174
|
+
if not header.document or (
|
|
175
|
+
not header.document.get("id")
|
|
176
|
+
and not header.document.get("link")
|
|
177
|
+
):
|
|
178
|
+
return MessageResult(
|
|
179
|
+
success=False,
|
|
180
|
+
error="Document header must include either 'id' or 'link'",
|
|
181
|
+
error_code="INVALID_MEDIA_HEADER",
|
|
182
|
+
recipient=to,
|
|
183
|
+
platform=PlatformType.WHATSAPP,
|
|
184
|
+
tenant_id=self._tenant_id,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Construct button objects with individual validation
|
|
188
|
+
formatted_buttons = []
|
|
189
|
+
for button in buttons:
|
|
190
|
+
if len(button.title) > 20:
|
|
191
|
+
return MessageResult(
|
|
192
|
+
success=False,
|
|
193
|
+
error=f"Button title '{button.title}' exceeds 20 characters",
|
|
194
|
+
error_code="BUTTON_TITLE_TOO_LONG",
|
|
195
|
+
recipient=to,
|
|
196
|
+
platform=PlatformType.WHATSAPP,
|
|
197
|
+
tenant_id=self._tenant_id,
|
|
198
|
+
)
|
|
199
|
+
if len(button.id) > 256:
|
|
200
|
+
return MessageResult(
|
|
201
|
+
success=False,
|
|
202
|
+
error=f"Button ID '{button.id}' exceeds 256 characters",
|
|
203
|
+
error_code="BUTTON_ID_TOO_LONG",
|
|
204
|
+
recipient=to,
|
|
205
|
+
platform=PlatformType.WHATSAPP,
|
|
206
|
+
tenant_id=self._tenant_id,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
formatted_buttons.append(
|
|
210
|
+
{"type": "reply", "reply": {"id": button.id, "title": button.title}}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Construct payload
|
|
214
|
+
payload = {
|
|
215
|
+
"messaging_product": "whatsapp",
|
|
216
|
+
"recipient_type": "individual",
|
|
217
|
+
"to": to,
|
|
218
|
+
"type": "interactive",
|
|
219
|
+
"interactive": {
|
|
220
|
+
"type": "button",
|
|
221
|
+
"body": {"text": body},
|
|
222
|
+
"action": {"buttons": formatted_buttons},
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# Add reply context if specified
|
|
227
|
+
if reply_to_message_id:
|
|
228
|
+
payload["context"] = {"message_id": reply_to_message_id}
|
|
229
|
+
|
|
230
|
+
# Add header if specified
|
|
231
|
+
if header:
|
|
232
|
+
# Convert InteractiveHeader model to dict format for API
|
|
233
|
+
header_dict = {
|
|
234
|
+
"type": header.type.value # Convert enum to string
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# Add type-specific content
|
|
238
|
+
if header.type == HeaderType.TEXT:
|
|
239
|
+
header_dict["text"] = header.text
|
|
240
|
+
elif header.type == HeaderType.IMAGE and header.image:
|
|
241
|
+
header_dict["image"] = header.image
|
|
242
|
+
elif header.type == HeaderType.VIDEO and header.video:
|
|
243
|
+
header_dict["video"] = header.video
|
|
244
|
+
elif header.type == HeaderType.DOCUMENT and header.document:
|
|
245
|
+
header_dict["document"] = header.document
|
|
246
|
+
|
|
247
|
+
payload["interactive"]["header"] = header_dict
|
|
248
|
+
|
|
249
|
+
# Add footer if specified
|
|
250
|
+
if footer_text:
|
|
251
|
+
payload["interactive"]["footer"] = {"text": footer_text}
|
|
252
|
+
|
|
253
|
+
self.logger.debug(
|
|
254
|
+
f"Sending interactive button menu to {to} with {len(buttons)} buttons"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Send using WhatsApp client
|
|
258
|
+
response = await self.client.post_request(payload)
|
|
259
|
+
|
|
260
|
+
message_id = response.get("messages", [{}])[0].get("id")
|
|
261
|
+
self.logger.info(
|
|
262
|
+
f"Interactive button menu sent successfully to {to}, id: {message_id}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return MessageResult(
|
|
266
|
+
success=True,
|
|
267
|
+
message_id=message_id,
|
|
268
|
+
recipient=to,
|
|
269
|
+
platform=PlatformType.WHATSAPP,
|
|
270
|
+
tenant_id=self._tenant_id,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
# Check for authentication errors
|
|
275
|
+
if "401" in str(e) or "Unauthorized" in str(e):
|
|
276
|
+
self.logger.error(
|
|
277
|
+
"🚨 CRITICAL: WhatsApp Authentication Failed - Cannot Send Interactive Messages! 🚨"
|
|
278
|
+
)
|
|
279
|
+
self.logger.error(
|
|
280
|
+
f"🚨 Check WhatsApp access token for tenant {self._tenant_id}"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
self.logger.error(
|
|
284
|
+
f"Failed to send interactive button menu to {to}: {str(e)}"
|
|
285
|
+
)
|
|
286
|
+
return MessageResult(
|
|
287
|
+
success=False,
|
|
288
|
+
error=str(e),
|
|
289
|
+
recipient=to,
|
|
290
|
+
platform=PlatformType.WHATSAPP,
|
|
291
|
+
tenant_id=self._tenant_id,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
async def send_list_menu(
|
|
295
|
+
self,
|
|
296
|
+
to: str,
|
|
297
|
+
body: str,
|
|
298
|
+
button_text: str,
|
|
299
|
+
sections: list[dict],
|
|
300
|
+
header: str | None = None,
|
|
301
|
+
footer_text: str | None = None,
|
|
302
|
+
reply_to_message_id: str | None = None,
|
|
303
|
+
) -> MessageResult:
|
|
304
|
+
"""
|
|
305
|
+
Send an interactive list menu message via WhatsApp.
|
|
306
|
+
|
|
307
|
+
Based on existing WhatsAppServiceInteractive.send_list_menu() with
|
|
308
|
+
improved error handling, logging, and result structure.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
to: Recipient's phone number
|
|
312
|
+
body: Main message text (max 4096 chars)
|
|
313
|
+
button_text: Text for the button that opens the list (max 20 chars)
|
|
314
|
+
sections: List of section objects with format:
|
|
315
|
+
{
|
|
316
|
+
"title": "Section Title", # max 24 chars
|
|
317
|
+
"rows": [
|
|
318
|
+
{
|
|
319
|
+
"id": "unique_id", # max 200 chars
|
|
320
|
+
"title": "Row Title", # max 24 chars
|
|
321
|
+
"description": "Optional description" # max 72 chars
|
|
322
|
+
},
|
|
323
|
+
...
|
|
324
|
+
]
|
|
325
|
+
}
|
|
326
|
+
header: Header text (max 60 chars)
|
|
327
|
+
footer_text: Footer text (max 60 chars)
|
|
328
|
+
reply_to_message_id: Optional message ID to reply to
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
MessageResult with operation status and metadata
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
# Validate input parameters
|
|
335
|
+
if len(body) > 4096:
|
|
336
|
+
return MessageResult(
|
|
337
|
+
success=False,
|
|
338
|
+
error="Body text cannot exceed 4096 characters",
|
|
339
|
+
error_code="BODY_TOO_LONG",
|
|
340
|
+
recipient=to,
|
|
341
|
+
platform=PlatformType.WHATSAPP,
|
|
342
|
+
tenant_id=self._tenant_id,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if len(button_text) > 20:
|
|
346
|
+
self.logger.error(
|
|
347
|
+
f"⚠️ WhatsApp List Button Text Validation Failed: '{button_text}' "
|
|
348
|
+
f"({len(button_text)} chars) exceeds 20 character limit. "
|
|
349
|
+
f"Please shorten the button text in your configuration."
|
|
350
|
+
)
|
|
351
|
+
return MessageResult(
|
|
352
|
+
success=False,
|
|
353
|
+
error=f"Button text '{button_text}' ({len(button_text)} chars) exceeds 20 character limit",
|
|
354
|
+
error_code="BUTTON_TEXT_TOO_LONG",
|
|
355
|
+
recipient=to,
|
|
356
|
+
platform=PlatformType.WHATSAPP,
|
|
357
|
+
tenant_id=self._tenant_id,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if len(sections) > 10:
|
|
361
|
+
return MessageResult(
|
|
362
|
+
success=False,
|
|
363
|
+
error="Maximum of 10 sections allowed",
|
|
364
|
+
error_code="TOO_MANY_SECTIONS",
|
|
365
|
+
recipient=to,
|
|
366
|
+
platform=PlatformType.WHATSAPP,
|
|
367
|
+
tenant_id=self._tenant_id,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
if header and len(header) > 60:
|
|
371
|
+
return MessageResult(
|
|
372
|
+
success=False,
|
|
373
|
+
error="Header text cannot exceed 60 characters",
|
|
374
|
+
error_code="HEADER_TOO_LONG",
|
|
375
|
+
recipient=to,
|
|
376
|
+
platform=PlatformType.WHATSAPP,
|
|
377
|
+
tenant_id=self._tenant_id,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if footer_text and len(footer_text) > 60:
|
|
381
|
+
return MessageResult(
|
|
382
|
+
success=False,
|
|
383
|
+
error="Footer text cannot exceed 60 characters",
|
|
384
|
+
error_code="FOOTER_TOO_LONG",
|
|
385
|
+
recipient=to,
|
|
386
|
+
platform=PlatformType.WHATSAPP,
|
|
387
|
+
tenant_id=self._tenant_id,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Validate and format sections
|
|
391
|
+
formatted_sections = []
|
|
392
|
+
all_row_ids = []
|
|
393
|
+
|
|
394
|
+
for section in sections:
|
|
395
|
+
if len(section["title"]) > 24:
|
|
396
|
+
return MessageResult(
|
|
397
|
+
success=False,
|
|
398
|
+
error=f"Section title '{section['title']}' exceeds 24 characters",
|
|
399
|
+
error_code="SECTION_TITLE_TOO_LONG",
|
|
400
|
+
recipient=to,
|
|
401
|
+
platform=PlatformType.WHATSAPP,
|
|
402
|
+
tenant_id=self._tenant_id,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if len(section["rows"]) > 10:
|
|
406
|
+
return MessageResult(
|
|
407
|
+
success=False,
|
|
408
|
+
error=f"Section '{section['title']}' has more than 10 rows",
|
|
409
|
+
error_code="TOO_MANY_ROWS",
|
|
410
|
+
recipient=to,
|
|
411
|
+
platform=PlatformType.WHATSAPP,
|
|
412
|
+
tenant_id=self._tenant_id,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
formatted_rows = []
|
|
416
|
+
for row in section["rows"]:
|
|
417
|
+
if len(row["id"]) > 200:
|
|
418
|
+
return MessageResult(
|
|
419
|
+
success=False,
|
|
420
|
+
error=f"Row ID '{row['id']}' exceeds 200 characters",
|
|
421
|
+
error_code="ROW_ID_TOO_LONG",
|
|
422
|
+
recipient=to,
|
|
423
|
+
platform=PlatformType.WHATSAPP,
|
|
424
|
+
tenant_id=self._tenant_id,
|
|
425
|
+
)
|
|
426
|
+
if len(row["title"]) > 24:
|
|
427
|
+
return MessageResult(
|
|
428
|
+
success=False,
|
|
429
|
+
error=f"Row title '{row['title']}' exceeds 24 characters",
|
|
430
|
+
error_code="ROW_TITLE_TOO_LONG",
|
|
431
|
+
recipient=to,
|
|
432
|
+
platform=PlatformType.WHATSAPP,
|
|
433
|
+
tenant_id=self._tenant_id,
|
|
434
|
+
)
|
|
435
|
+
if "description" in row and len(row["description"]) > 72:
|
|
436
|
+
return MessageResult(
|
|
437
|
+
success=False,
|
|
438
|
+
error=f"Row description for '{row['title']}' exceeds 72 characters",
|
|
439
|
+
error_code="ROW_DESCRIPTION_TOO_LONG",
|
|
440
|
+
recipient=to,
|
|
441
|
+
platform=PlatformType.WHATSAPP,
|
|
442
|
+
tenant_id=self._tenant_id,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Check for duplicate row IDs
|
|
446
|
+
if row["id"] in all_row_ids:
|
|
447
|
+
return MessageResult(
|
|
448
|
+
success=False,
|
|
449
|
+
error=f"Row ID '{row['id']}' is not unique",
|
|
450
|
+
error_code="DUPLICATE_ROW_ID",
|
|
451
|
+
recipient=to,
|
|
452
|
+
platform=PlatformType.WHATSAPP,
|
|
453
|
+
tenant_id=self._tenant_id,
|
|
454
|
+
)
|
|
455
|
+
all_row_ids.append(row["id"])
|
|
456
|
+
|
|
457
|
+
formatted_row = {"id": row["id"], "title": row["title"]}
|
|
458
|
+
if "description" in row:
|
|
459
|
+
formatted_row["description"] = row["description"]
|
|
460
|
+
formatted_rows.append(formatted_row)
|
|
461
|
+
|
|
462
|
+
formatted_sections.append(
|
|
463
|
+
{"title": section["title"], "rows": formatted_rows}
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# Construct payload
|
|
467
|
+
payload = {
|
|
468
|
+
"messaging_product": "whatsapp",
|
|
469
|
+
"recipient_type": "individual",
|
|
470
|
+
"to": to,
|
|
471
|
+
"type": "interactive",
|
|
472
|
+
"interactive": {
|
|
473
|
+
"type": "list",
|
|
474
|
+
"body": {"text": body},
|
|
475
|
+
"action": {"button": button_text, "sections": formatted_sections},
|
|
476
|
+
},
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
# Add reply context if specified
|
|
480
|
+
if reply_to_message_id:
|
|
481
|
+
payload["context"] = {"message_id": reply_to_message_id}
|
|
482
|
+
|
|
483
|
+
# Add header if specified
|
|
484
|
+
if header:
|
|
485
|
+
payload["interactive"]["header"] = {"type": "text", "text": header}
|
|
486
|
+
|
|
487
|
+
# Add footer if specified
|
|
488
|
+
if footer_text:
|
|
489
|
+
payload["interactive"]["footer"] = {"text": footer_text}
|
|
490
|
+
|
|
491
|
+
self.logger.debug(
|
|
492
|
+
f"Sending list menu message to {to} with {len(sections)} sections"
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# Send using WhatsApp client
|
|
496
|
+
response = await self.client.post_request(payload)
|
|
497
|
+
|
|
498
|
+
message_id = response.get("messages", [{}])[0].get("id")
|
|
499
|
+
self.logger.info(
|
|
500
|
+
f"List menu message sent successfully to {to}, id: {message_id}"
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
return MessageResult(
|
|
504
|
+
success=True,
|
|
505
|
+
message_id=message_id,
|
|
506
|
+
recipient=to,
|
|
507
|
+
platform=PlatformType.WHATSAPP,
|
|
508
|
+
tenant_id=self._tenant_id,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
except Exception as e:
|
|
512
|
+
# Check for authentication errors
|
|
513
|
+
if "401" in str(e) or "Unauthorized" in str(e):
|
|
514
|
+
self.logger.error(
|
|
515
|
+
"🚨 CRITICAL: WhatsApp Authentication Failed - Cannot Send List Messages! 🚨"
|
|
516
|
+
)
|
|
517
|
+
self.logger.error(
|
|
518
|
+
f"🚨 Check WhatsApp access token for tenant {self._tenant_id}"
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
self.logger.error(
|
|
522
|
+
f"❌ Failed to send list menu to {to}: {str(e)} - "
|
|
523
|
+
f"button_text: '{button_text}', sections_count: {len(sections)}, "
|
|
524
|
+
f"body_length: {len(body)}",
|
|
525
|
+
exc_info=True,
|
|
526
|
+
)
|
|
527
|
+
return MessageResult(
|
|
528
|
+
success=False,
|
|
529
|
+
error=str(e),
|
|
530
|
+
recipient=to,
|
|
531
|
+
platform=PlatformType.WHATSAPP,
|
|
532
|
+
tenant_id=self._tenant_id,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
async def send_cta_button(
|
|
536
|
+
self,
|
|
537
|
+
to: str,
|
|
538
|
+
body: str,
|
|
539
|
+
button_text: str,
|
|
540
|
+
button_url: str,
|
|
541
|
+
header_text: str | None = None,
|
|
542
|
+
footer_text: str | None = None,
|
|
543
|
+
reply_to_message_id: str | None = None,
|
|
544
|
+
) -> MessageResult:
|
|
545
|
+
"""
|
|
546
|
+
Send an interactive Call-to-Action URL button message via WhatsApp.
|
|
547
|
+
|
|
548
|
+
Based on existing WhatsAppServiceInteractive.send_cta_button() with
|
|
549
|
+
improved error handling, logging, and result structure.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
to: Recipient's phone number
|
|
553
|
+
body: Required. Message body text
|
|
554
|
+
button_text: Required. Text to display on the button
|
|
555
|
+
button_url: Required. URL to load when button is tapped
|
|
556
|
+
header_text: Text to display in the header
|
|
557
|
+
footer_text: Text to display in the footer
|
|
558
|
+
reply_to_message_id: Optional message ID to reply to
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
MessageResult with operation status and metadata
|
|
562
|
+
"""
|
|
563
|
+
try:
|
|
564
|
+
# Validate required parameters
|
|
565
|
+
if not all([body, button_text, button_url]):
|
|
566
|
+
return MessageResult(
|
|
567
|
+
success=False,
|
|
568
|
+
error="body, button_text, and button_url are required parameters",
|
|
569
|
+
error_code="MISSING_REQUIRED_PARAMS",
|
|
570
|
+
recipient=to,
|
|
571
|
+
platform=PlatformType.WHATSAPP,
|
|
572
|
+
tenant_id=self._tenant_id,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Validate URL format
|
|
576
|
+
if not (
|
|
577
|
+
button_url.startswith("http://") or button_url.startswith("https://")
|
|
578
|
+
):
|
|
579
|
+
return MessageResult(
|
|
580
|
+
success=False,
|
|
581
|
+
error="button_url must start with http:// or https://",
|
|
582
|
+
error_code="INVALID_URL_FORMAT",
|
|
583
|
+
recipient=to,
|
|
584
|
+
platform=PlatformType.WHATSAPP,
|
|
585
|
+
tenant_id=self._tenant_id,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
# Construct payload
|
|
589
|
+
payload = {
|
|
590
|
+
"messaging_product": "whatsapp",
|
|
591
|
+
"recipient_type": "individual",
|
|
592
|
+
"to": to,
|
|
593
|
+
"type": "interactive",
|
|
594
|
+
"interactive": {
|
|
595
|
+
"type": "cta_url",
|
|
596
|
+
"body": {"text": body},
|
|
597
|
+
"action": {
|
|
598
|
+
"name": "cta_url",
|
|
599
|
+
"parameters": {"display_text": button_text, "url": button_url},
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
# Add reply context if specified
|
|
605
|
+
if reply_to_message_id:
|
|
606
|
+
payload["context"] = {"message_id": reply_to_message_id}
|
|
607
|
+
|
|
608
|
+
# Add optional header if provided
|
|
609
|
+
if header_text:
|
|
610
|
+
payload["interactive"]["header"] = {"type": "text", "text": header_text}
|
|
611
|
+
|
|
612
|
+
# Add optional footer if provided
|
|
613
|
+
if footer_text:
|
|
614
|
+
payload["interactive"]["footer"] = {"text": footer_text}
|
|
615
|
+
|
|
616
|
+
self.logger.debug(
|
|
617
|
+
f"Sending CTA button message to {to} with URL: {button_url}"
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
# Send using WhatsApp client
|
|
621
|
+
response = await self.client.post_request(payload)
|
|
622
|
+
|
|
623
|
+
message_id = response.get("messages", [{}])[0].get("id")
|
|
624
|
+
self.logger.info(
|
|
625
|
+
f"CTA button message sent successfully to {to}, id: {message_id}"
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
return MessageResult(
|
|
629
|
+
success=True,
|
|
630
|
+
message_id=message_id,
|
|
631
|
+
recipient=to,
|
|
632
|
+
platform=PlatformType.WHATSAPP,
|
|
633
|
+
tenant_id=self._tenant_id,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
except Exception as e:
|
|
637
|
+
# Check for authentication errors
|
|
638
|
+
if "401" in str(e) or "Unauthorized" in str(e):
|
|
639
|
+
self.logger.error(
|
|
640
|
+
"🚨 CRITICAL: WhatsApp Authentication Failed - Cannot Send CTA Messages! 🚨"
|
|
641
|
+
)
|
|
642
|
+
self.logger.error(
|
|
643
|
+
f"🚨 Check WhatsApp access token for tenant {self._tenant_id}"
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
self.logger.error(f"Failed to send CTA button message to {to}: {str(e)}")
|
|
647
|
+
return MessageResult(
|
|
648
|
+
success=False,
|
|
649
|
+
error=str(e),
|
|
650
|
+
recipient=to,
|
|
651
|
+
platform=PlatformType.WHATSAPP,
|
|
652
|
+
tenant_id=self._tenant_id,
|
|
653
|
+
)
|