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