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,904 @@
1
+ """
2
+ WhatsApp unified implementation of the IMessenger interface.
3
+
4
+ Provides complete WhatsApp-specific implementation of ALL messaging operations:
5
+ - Basic messaging: send_text, mark_as_read
6
+ - Media messaging: send_image, send_video, send_audio, send_document, send_sticker
7
+ - Interactive messaging: send_button_message, send_list_message, send_cta_message
8
+ - Template messaging: send_text_template, send_media_template, send_location_template
9
+ - Specialized messaging: send_contact, send_location, send_location_request
10
+
11
+ This is the ONLY WhatsApp messenger implementation that should be used.
12
+ It replaces the previous partial implementations (WhatsAppBasicMessenger and WhatsAppMediaMessenger)
13
+ which violated the Interface Segregation Principle.
14
+ """
15
+
16
+ from pathlib import Path
17
+
18
+ from wappa.core.logging.logger import get_logger
19
+ from wappa.domain.interfaces.messaging_interface import IMessenger
20
+ from wappa.messaging.whatsapp.client.whatsapp_client import WhatsAppClient
21
+ from wappa.messaging.whatsapp.handlers.whatsapp_interactive_handler import (
22
+ WhatsAppInteractiveHandler,
23
+ )
24
+ from wappa.messaging.whatsapp.handlers.whatsapp_media_handler import (
25
+ WhatsAppMediaHandler,
26
+ )
27
+ from wappa.messaging.whatsapp.handlers.whatsapp_specialized_handler import (
28
+ WhatsAppSpecializedHandler,
29
+ )
30
+ from wappa.messaging.whatsapp.handlers.whatsapp_template_handler import (
31
+ WhatsAppTemplateHandler,
32
+ )
33
+ from wappa.messaging.whatsapp.models.basic_models import MessageResult
34
+ from wappa.messaging.whatsapp.models.interactive_models import (
35
+ InteractiveHeader,
36
+ ReplyButton,
37
+ )
38
+ from wappa.messaging.whatsapp.models.media_models import MediaType
39
+ from wappa.schemas.core.types import PlatformType
40
+
41
+
42
+ class WhatsAppMessenger(IMessenger):
43
+ """
44
+ Complete WhatsApp implementation of the messaging interface.
45
+
46
+ Provides ALL messaging functionality using WhatsApp Business API:
47
+ - Basic messaging: send_text, mark_as_read
48
+ - Media messaging: send_image, send_video, send_audio, send_document, send_sticker
49
+ - Interactive messaging: send_button_message, send_list_message, send_cta_message
50
+ - Template messaging: send_text_template, send_media_template, send_location_template
51
+ - Specialized messaging: send_contact, send_location, send_location_request
52
+
53
+ Uses composition pattern with:
54
+ - WhatsAppClient: For basic API operations and text messaging
55
+ - WhatsAppMediaHandler: For media upload/download operations
56
+ - WhatsAppInteractiveHandler: For interactive message operations
57
+ - WhatsAppTemplateHandler: For business template message operations
58
+ - WhatsAppSpecializedHandler: For contact and location message operations
59
+
60
+ This unified implementation ensures complete IMessenger interface compliance
61
+ and eliminates the architectural violation of partial implementations.
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ client: WhatsAppClient,
67
+ media_handler: WhatsAppMediaHandler,
68
+ interactive_handler: WhatsAppInteractiveHandler,
69
+ template_handler: WhatsAppTemplateHandler,
70
+ specialized_handler: WhatsAppSpecializedHandler,
71
+ tenant_id: str,
72
+ ):
73
+ """Initialize unified WhatsApp messenger with complete functionality.
74
+
75
+ Args:
76
+ client: Configured WhatsApp client for API operations
77
+ media_handler: Media handler for upload/download operations
78
+ interactive_handler: Interactive handler for button/list/CTA operations
79
+ template_handler: Template handler for business template operations
80
+ specialized_handler: Specialized handler for contact/location operations
81
+ tenant_id: Tenant identifier (phone_number_id in WhatsApp context)
82
+ """
83
+ self.client = client
84
+ self.media_handler = media_handler
85
+ self.interactive_handler = interactive_handler
86
+ self.template_handler = template_handler
87
+ self.specialized_handler = specialized_handler
88
+ self._tenant_id = tenant_id
89
+ self.logger = get_logger(__name__)
90
+
91
+ @property
92
+ def platform(self) -> PlatformType:
93
+ """Get the platform this messenger handles."""
94
+ return PlatformType.WHATSAPP
95
+
96
+ @property
97
+ def tenant_id(self) -> str:
98
+ """Get the tenant ID this messenger serves."""
99
+ return self._tenant_id
100
+
101
+ # Basic Messaging Methods (from WhatsAppBasicMessenger)
102
+
103
+ async def send_text(
104
+ self,
105
+ text: str,
106
+ recipient: str,
107
+ reply_to_message_id: str | None = None,
108
+ disable_preview: bool = False,
109
+ ) -> MessageResult:
110
+ """Send text message using WhatsApp API.
111
+
112
+ Args:
113
+ text: Text content of the message (1-4096 characters)
114
+ recipient: Recipient phone number
115
+ reply_to_message_id: Optional message ID to reply to
116
+ disable_preview: Whether to disable URL preview
117
+
118
+ Returns:
119
+ MessageResult with operation status and metadata
120
+ """
121
+ try:
122
+ # Check for URLs for preview control
123
+ has_url = "http://" in text or "https://" in text
124
+
125
+ # Create WhatsApp-specific payload
126
+ payload = {
127
+ "messaging_product": "whatsapp",
128
+ "to": recipient,
129
+ "type": "text",
130
+ "text": {"body": text, "preview_url": has_url and not disable_preview},
131
+ }
132
+
133
+ # Add reply context if specified
134
+ if reply_to_message_id:
135
+ payload["context"] = {"message_id": reply_to_message_id}
136
+
137
+ self.logger.debug(f"Sending text message to {recipient}: {text[:50]}...")
138
+ response = await self.client.post_request(payload)
139
+
140
+ message_id = response.get("messages", [{}])[0].get("id")
141
+ self.logger.info(
142
+ f"Text message sent successfully to {recipient}, id: {message_id}"
143
+ )
144
+
145
+ return MessageResult(
146
+ success=True,
147
+ message_id=message_id,
148
+ recipient=recipient,
149
+ platform=PlatformType.WHATSAPP,
150
+ tenant_id=self._tenant_id,
151
+ )
152
+
153
+ except Exception as e:
154
+ # Check for authentication errors
155
+ if "401" in str(e) or "Unauthorized" in str(e):
156
+ self.logger.error(
157
+ "🚨 CRITICAL: WhatsApp Authentication Failed - Cannot Send Messages! 🚨"
158
+ )
159
+ self.logger.error(
160
+ f"🚨 Check WhatsApp access token for tenant {self._tenant_id}"
161
+ )
162
+
163
+ self.logger.error(f"Failed to send text to {recipient}: {str(e)}")
164
+ return MessageResult(
165
+ success=False,
166
+ error=str(e),
167
+ recipient=recipient,
168
+ platform=PlatformType.WHATSAPP,
169
+ tenant_id=self._tenant_id,
170
+ )
171
+
172
+ async def mark_as_read(
173
+ self, message_id: str, typing: bool = False
174
+ ) -> MessageResult:
175
+ """Mark message as read, optionally with typing indicator.
176
+
177
+ WhatsApp Business API requires separate calls for:
178
+ 1. Marking message as read (status endpoint)
179
+ 2. Sending typing indicator (separate action)
180
+
181
+ Args:
182
+ message_id: WhatsApp message ID to mark as read
183
+ typing: Whether to show typing indicator after marking as read
184
+
185
+ Returns:
186
+ MessageResult with operation status and metadata
187
+ """
188
+ try:
189
+ # Step 1: Mark message as read
190
+ read_payload = {
191
+ "messaging_product": "whatsapp",
192
+ "status": "read",
193
+ "message_id": message_id,
194
+ }
195
+
196
+ self.logger.debug(f"Marking message {message_id} as read")
197
+ await self.client.post_request(read_payload)
198
+
199
+ # Step 2: Send typing indicator if requested (separate API call)
200
+ if typing:
201
+ # Extract recipient from message_id context or use a separate parameter
202
+ # For now, we'll skip the typing indicator to avoid the 401 error
203
+ # TODO: Implement proper typing indicator with recipient WhatsApp ID
204
+ self.logger.debug(
205
+ "Typing indicator requested but skipped (requires recipient ID)"
206
+ )
207
+
208
+ action_msg = (
209
+ "marked as read with typing indicator" if typing else "marked as read"
210
+ )
211
+ self.logger.info(f"Message {message_id} {action_msg}")
212
+
213
+ return MessageResult(
214
+ success=True,
215
+ message_id=message_id,
216
+ platform=PlatformType.WHATSAPP,
217
+ tenant_id=self._tenant_id,
218
+ )
219
+
220
+ except Exception as e:
221
+ action_msg = (
222
+ "mark as read with typing indicator" if typing else "mark as read"
223
+ )
224
+
225
+ # Check for authentication errors
226
+ if "401" in str(e) or "Unauthorized" in str(e):
227
+ self.logger.error(
228
+ "🚨 CRITICAL: WhatsApp Authentication Failed - Cannot Mark Messages as Read! 🚨"
229
+ )
230
+ self.logger.error(
231
+ f"🚨 Check WhatsApp access token for tenant {self._tenant_id}"
232
+ )
233
+
234
+ self.logger.error(f"Failed to {action_msg} message {message_id}: {str(e)}")
235
+ return MessageResult(
236
+ success=False,
237
+ error=str(e),
238
+ platform=PlatformType.WHATSAPP,
239
+ tenant_id=self._tenant_id,
240
+ )
241
+
242
+ # Media Messaging Methods (from WhatsAppMediaMessenger)
243
+
244
+ async def send_image(
245
+ self,
246
+ image_source: str | Path,
247
+ recipient: str,
248
+ caption: str | None = None,
249
+ reply_to_message_id: str | None = None,
250
+ ) -> MessageResult:
251
+ """Send image message using WhatsApp API.
252
+
253
+ Supports JPEG and PNG images up to 5MB.
254
+ Images must be 8-bit, RGB or RGBA (WhatsApp Cloud API 2025).
255
+
256
+ Args:
257
+ image_source: Image URL or file path
258
+ recipient: Recipient identifier
259
+ caption: Optional caption for the image (max 1024 characters)
260
+ reply_to_message_id: Optional message ID to reply to
261
+
262
+ Returns:
263
+ MessageResult with operation status and metadata
264
+ """
265
+ return await self._send_media(
266
+ media_source=image_source,
267
+ media_type=MediaType.IMAGE,
268
+ recipient=recipient,
269
+ caption=caption,
270
+ filename=None,
271
+ reply_to_message_id=reply_to_message_id,
272
+ )
273
+
274
+ async def send_video(
275
+ self,
276
+ video_source: str | Path,
277
+ recipient: str,
278
+ caption: str | None = None,
279
+ reply_to_message_id: str | None = None,
280
+ ) -> MessageResult:
281
+ """Send video message using WhatsApp API.
282
+
283
+ Supports MP4 and 3GP videos up to 16MB.
284
+ Only H.264 video codec and AAC audio codec supported.
285
+ Single audio stream or no audio stream only (WhatsApp Cloud API 2025).
286
+
287
+ Args:
288
+ video_source: Video URL or file path
289
+ recipient: Recipient identifier
290
+ caption: Optional caption for the video (max 1024 characters)
291
+ reply_to_message_id: Optional message ID to reply to
292
+
293
+ Returns:
294
+ MessageResult with operation status and metadata
295
+ """
296
+ return await self._send_media(
297
+ media_source=video_source,
298
+ media_type=MediaType.VIDEO,
299
+ recipient=recipient,
300
+ caption=caption,
301
+ filename=None,
302
+ reply_to_message_id=reply_to_message_id,
303
+ )
304
+
305
+ async def send_audio(
306
+ self,
307
+ audio_source: str | Path,
308
+ recipient: str,
309
+ reply_to_message_id: str | None = None,
310
+ ) -> MessageResult:
311
+ """Send audio message using WhatsApp API.
312
+
313
+ Supports AAC, AMR, MP3, M4A, and OGG audio up to 16MB.
314
+ OGG must use OPUS codecs only, mono input only (WhatsApp Cloud API 2025).
315
+
316
+ Args:
317
+ audio_source: Audio URL or file path
318
+ recipient: Recipient identifier
319
+ reply_to_message_id: Optional message ID to reply to
320
+
321
+ Returns:
322
+ MessageResult with operation status and metadata
323
+
324
+ Note:
325
+ Audio messages do not support captions.
326
+ """
327
+ return await self._send_media(
328
+ media_source=audio_source,
329
+ media_type=MediaType.AUDIO,
330
+ recipient=recipient,
331
+ caption=None, # Audio doesn't support captions
332
+ filename=None,
333
+ reply_to_message_id=reply_to_message_id,
334
+ )
335
+
336
+ async def send_document(
337
+ self,
338
+ document_source: str | Path,
339
+ recipient: str,
340
+ filename: str | None = None,
341
+ caption: str | None = None,
342
+ reply_to_message_id: str | None = None,
343
+ ) -> MessageResult:
344
+ """Send document message using WhatsApp API.
345
+
346
+ Supports TXT, PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX up to 100MB.
347
+
348
+ Args:
349
+ document_source: Document URL or file path
350
+ recipient: Recipient identifier
351
+ filename: Optional filename for the document
352
+ caption: Optional caption for the document (max 1024 characters)
353
+ reply_to_message_id: Optional message ID to reply to
354
+
355
+ Returns:
356
+ MessageResult with operation status and metadata
357
+ """
358
+ return await self._send_media(
359
+ media_source=document_source,
360
+ media_type=MediaType.DOCUMENT,
361
+ recipient=recipient,
362
+ caption=caption, # Documents DO support captions in WhatsApp Business API
363
+ filename=filename,
364
+ reply_to_message_id=reply_to_message_id,
365
+ )
366
+
367
+ async def send_sticker(
368
+ self,
369
+ sticker_source: str | Path,
370
+ recipient: str,
371
+ reply_to_message_id: str | None = None,
372
+ ) -> MessageResult:
373
+ """Send sticker message using WhatsApp API.
374
+
375
+ Supports WebP images only.
376
+ Static stickers: 100KB max, Animated stickers: 500KB max.
377
+
378
+ Args:
379
+ sticker_source: Sticker URL or file path (WebP format)
380
+ recipient: Recipient identifier
381
+ reply_to_message_id: Optional message ID to reply to
382
+
383
+ Returns:
384
+ MessageResult with operation status and metadata
385
+
386
+ Note:
387
+ Sticker messages do not support captions.
388
+ """
389
+ return await self._send_media(
390
+ media_source=sticker_source,
391
+ media_type=MediaType.STICKER,
392
+ recipient=recipient,
393
+ caption=None, # Stickers don't support captions
394
+ filename=None,
395
+ reply_to_message_id=reply_to_message_id,
396
+ )
397
+
398
+ async def _send_media(
399
+ self,
400
+ media_source: str | Path,
401
+ media_type: MediaType,
402
+ recipient: str,
403
+ caption: str | None = None,
404
+ filename: str | None = None,
405
+ reply_to_message_id: str | None = None,
406
+ ) -> MessageResult:
407
+ """
408
+ Internal method to send media messages.
409
+
410
+ Handles both URL and file path sources with upload workflow.
411
+ Uses the injected media handler for upload operations.
412
+ """
413
+ try:
414
+ # Build initial payload
415
+ payload = {
416
+ "messaging_product": "whatsapp",
417
+ "recipient_type": "individual",
418
+ "to": recipient,
419
+ "type": media_type.value,
420
+ }
421
+
422
+ if reply_to_message_id:
423
+ payload["context"] = {"message_id": reply_to_message_id}
424
+
425
+ # Handle media source: URL vs file path
426
+ if isinstance(media_source, str) and (
427
+ media_source.startswith("http://")
428
+ or media_source.startswith("https://")
429
+ ):
430
+ # Use URL directly (link-based object)
431
+ media_obj = {"link": media_source}
432
+ self.logger.debug(
433
+ f"Using media URL for {media_type.value}: {media_source}"
434
+ )
435
+ else:
436
+ # Upload local file first or use media_id if it's already an ID
437
+ if (
438
+ isinstance(media_source, str)
439
+ and len(media_source) < 100
440
+ and "/" not in media_source
441
+ ):
442
+ # Likely already a media_id from echo functionality
443
+ media_obj = {"id": media_source}
444
+ self.logger.debug(
445
+ f"Using existing media ID for {media_type.value}: {media_source}"
446
+ )
447
+ else:
448
+ # Upload local file
449
+ media_path = Path(media_source)
450
+ if not media_path.exists():
451
+ return MessageResult(
452
+ success=False,
453
+ error=f"Media file not found: {media_path}",
454
+ error_code="FILE_NOT_FOUND",
455
+ recipient=recipient,
456
+ platform=PlatformType.WHATSAPP,
457
+ tenant_id=self._tenant_id,
458
+ )
459
+
460
+ self.logger.debug(
461
+ f"Uploading media file for {media_type.value}: {media_path.name}"
462
+ )
463
+
464
+ # Upload using media handler
465
+ upload_result = await self.media_handler.upload_media(media_path)
466
+ if not upload_result.success:
467
+ return MessageResult(
468
+ success=False,
469
+ error=f"Failed to upload media: {upload_result.error}",
470
+ error_code=upload_result.error_code,
471
+ recipient=recipient,
472
+ platform=PlatformType.WHATSAPP,
473
+ tenant_id=self._tenant_id,
474
+ )
475
+
476
+ # Use uploaded media ID
477
+ media_obj = {"id": upload_result.media_id}
478
+ self.logger.debug(
479
+ f"Using uploaded media ID for {media_type.value}: {upload_result.media_id}"
480
+ )
481
+
482
+ # Add optional caption (if allowed) and filename (for documents)
483
+ if caption and media_type not in (MediaType.AUDIO, MediaType.STICKER):
484
+ media_obj["caption"] = caption
485
+
486
+ if media_type == MediaType.DOCUMENT and filename:
487
+ media_obj["filename"] = filename
488
+
489
+ # Set media object in payload
490
+ payload[media_type.value] = media_obj
491
+
492
+ self.logger.debug(f"Sending {media_type.value} message to {recipient}")
493
+
494
+ # Send message using client
495
+ response = await self.client.post_request(payload)
496
+
497
+ message_id = response.get("messages", [{}])[0].get("id")
498
+ self.logger.info(
499
+ f"{media_type.value.title()} message sent successfully to {recipient}, id: {message_id}"
500
+ )
501
+
502
+ return MessageResult(
503
+ success=True,
504
+ message_id=message_id,
505
+ recipient=recipient,
506
+ platform=PlatformType.WHATSAPP,
507
+ tenant_id=self._tenant_id,
508
+ )
509
+
510
+ except Exception as e:
511
+ # Check for authentication errors
512
+ if "401" in str(e) or "Unauthorized" in str(e):
513
+ self.logger.error(
514
+ "🚨 CRITICAL: WhatsApp Authentication Failed - Cannot Send Media Messages! 🚨"
515
+ )
516
+ self.logger.error(
517
+ f"🚨 Check WhatsApp access token for tenant {self._tenant_id}"
518
+ )
519
+
520
+ self.logger.error(
521
+ f"Failed to send {media_type.value} to {recipient}: {str(e)}"
522
+ )
523
+ return MessageResult(
524
+ success=False,
525
+ error=str(e),
526
+ recipient=recipient,
527
+ platform=PlatformType.WHATSAPP,
528
+ tenant_id=self._tenant_id,
529
+ )
530
+
531
+ # Interactive Messaging Methods (from WhatsAppInteractiveHandler)
532
+
533
+ async def send_button_message(
534
+ self,
535
+ buttons: list[ReplyButton],
536
+ recipient: str,
537
+ body: str,
538
+ header: InteractiveHeader | None = None,
539
+ footer: str | None = None,
540
+ reply_to_message_id: str | None = None,
541
+ ) -> MessageResult:
542
+ """Send interactive button message using WhatsApp API.
543
+
544
+ Supports up to 3 quick reply buttons with optional header and footer.
545
+ Based on WhatsApp Cloud API 2025 interactive button specifications.
546
+
547
+ Args:
548
+ buttons: List of ReplyButton models (max 3 buttons)
549
+ recipient: Recipient identifier
550
+ body: Main message text (max 1024 characters)
551
+ header: Optional InteractiveHeader model with type and content
552
+ footer: Optional footer text (max 60 characters)
553
+ reply_to_message_id: Optional message ID to reply to
554
+
555
+ Returns:
556
+ MessageResult with operation status and metadata
557
+ """
558
+ return await self.interactive_handler.send_buttons_menu(
559
+ to=recipient,
560
+ body=body,
561
+ buttons=buttons,
562
+ header=header,
563
+ footer_text=footer,
564
+ reply_to_message_id=reply_to_message_id,
565
+ )
566
+
567
+ async def send_list_message(
568
+ self,
569
+ sections: list[dict],
570
+ recipient: str,
571
+ body: str,
572
+ button_text: str,
573
+ header: str | None = None,
574
+ footer: str | None = None,
575
+ reply_to_message_id: str | None = None,
576
+ ) -> MessageResult:
577
+ """Send interactive list message using WhatsApp API.
578
+
579
+ Supports sectioned lists with rows (max 10 sections, 10 rows per section).
580
+ Based on WhatsApp Cloud API 2025 interactive list specifications.
581
+
582
+ Args:
583
+ sections: List of section objects with title and rows
584
+ recipient: Recipient identifier
585
+ body: Main message text (max 4096 characters)
586
+ button_text: Text for the button that opens the list (max 20 characters)
587
+ header: Optional header text (max 60 characters)
588
+ footer: Optional footer text (max 60 characters)
589
+ reply_to_message_id: Optional message ID to reply to
590
+
591
+ Returns:
592
+ MessageResult with operation status and metadata
593
+ """
594
+ return await self.interactive_handler.send_list_menu(
595
+ to=recipient,
596
+ body=body,
597
+ button_text=button_text,
598
+ sections=sections,
599
+ header=header,
600
+ footer_text=footer,
601
+ reply_to_message_id=reply_to_message_id,
602
+ )
603
+
604
+ async def send_cta_message(
605
+ self,
606
+ button_text: str,
607
+ button_url: str,
608
+ recipient: str,
609
+ body: str,
610
+ header: str | None = None,
611
+ footer: str | None = None,
612
+ reply_to_message_id: str | None = None,
613
+ ) -> MessageResult:
614
+ """Send interactive call-to-action URL button message using WhatsApp API.
615
+
616
+ Supports external URL buttons for call-to-action scenarios.
617
+ Based on WhatsApp Cloud API 2025 CTA URL specifications.
618
+
619
+ Args:
620
+ button_text: Text to display on the button
621
+ button_url: URL to load when button is tapped (must start with http:// or https://)
622
+ recipient: Recipient identifier
623
+ body: Main message text
624
+ header: Optional header text
625
+ footer: Optional footer text
626
+ reply_to_message_id: Optional message ID to reply to
627
+
628
+ Returns:
629
+ MessageResult with operation status and metadata
630
+ """
631
+ return await self.interactive_handler.send_cta_button(
632
+ to=recipient,
633
+ body=body,
634
+ button_text=button_text,
635
+ button_url=button_url,
636
+ header_text=header,
637
+ footer_text=footer,
638
+ reply_to_message_id=reply_to_message_id,
639
+ )
640
+
641
+ # Template Messaging Methods (from WhatsAppTemplateHandler)
642
+
643
+ async def send_text_template(
644
+ self,
645
+ template_name: str,
646
+ recipient: str,
647
+ body_parameters: list[dict] | None = None,
648
+ language_code: str = "es",
649
+ ) -> MessageResult:
650
+ """Send text-only template message using WhatsApp API.
651
+
652
+ Supports WhatsApp Business templates with parameter substitution.
653
+ Templates must be pre-approved by WhatsApp for use.
654
+
655
+ Args:
656
+ template_name: Name of the approved WhatsApp template
657
+ recipient: Recipient phone number
658
+ body_parameters: List of parameter objects for text replacement
659
+ language_code: BCP-47 language code for template (default: "es")
660
+
661
+ Returns:
662
+ MessageResult with operation status and metadata
663
+ """
664
+ # Convert Dict parameters to TemplateParameter objects if needed
665
+ template_parameters = None
666
+ if body_parameters:
667
+ from wappa.messaging.whatsapp.models.template_models import (
668
+ TemplateParameter,
669
+ TemplateParameterType,
670
+ )
671
+
672
+ template_parameters = []
673
+ for param in body_parameters:
674
+ if isinstance(param, dict) and param.get("type") == "text":
675
+ template_parameters.append(
676
+ TemplateParameter(
677
+ type=TemplateParameterType.TEXT, text=param.get("text")
678
+ )
679
+ )
680
+
681
+ return await self.template_handler.send_text_template(
682
+ phone_number=recipient,
683
+ template_name=template_name,
684
+ body_parameters=template_parameters,
685
+ language_code=language_code,
686
+ )
687
+
688
+ async def send_media_template(
689
+ self,
690
+ template_name: str,
691
+ recipient: str,
692
+ media_type: str,
693
+ media_id: str | None = None,
694
+ media_url: str | None = None,
695
+ body_parameters: list[dict] | None = None,
696
+ language_code: str = "es",
697
+ ) -> MessageResult:
698
+ """Send template message with media header using WhatsApp API.
699
+
700
+ Supports templates with image, video, or document headers.
701
+ Either media_id (uploaded media) or media_url (external media) must be provided.
702
+
703
+ Args:
704
+ template_name: Name of the approved WhatsApp template
705
+ recipient: Recipient phone number
706
+ media_type: Type of media header ("image", "video", "document")
707
+ media_id: ID of pre-uploaded media (exclusive with media_url)
708
+ media_url: URL of external media (exclusive with media_id)
709
+ body_parameters: List of parameter objects for text replacement
710
+ language_code: BCP-47 language code for template (default: "es")
711
+
712
+ Returns:
713
+ MessageResult with operation status and metadata
714
+ """
715
+ # Convert string media_type to MediaType enum
716
+ from wappa.messaging.whatsapp.models.template_models import MediaType
717
+
718
+ try:
719
+ media_type_enum = MediaType(media_type)
720
+ except ValueError:
721
+ return MessageResult(
722
+ success=False,
723
+ platform="whatsapp",
724
+ error=f"Invalid media type: {media_type}",
725
+ error_code="INVALID_MEDIA_TYPE",
726
+ )
727
+
728
+ # Convert Dict parameters to TemplateParameter objects if needed
729
+ template_parameters = None
730
+ if body_parameters:
731
+ from wappa.messaging.whatsapp.models.template_models import (
732
+ TemplateParameter,
733
+ TemplateParameterType,
734
+ )
735
+
736
+ template_parameters = []
737
+ for param in body_parameters:
738
+ if isinstance(param, dict) and param.get("type") == "text":
739
+ template_parameters.append(
740
+ TemplateParameter(
741
+ type=TemplateParameterType.TEXT, text=param.get("text")
742
+ )
743
+ )
744
+
745
+ return await self.template_handler.send_media_template(
746
+ phone_number=recipient,
747
+ template_name=template_name,
748
+ media_type=media_type_enum,
749
+ media_id=media_id,
750
+ media_url=media_url,
751
+ body_parameters=template_parameters,
752
+ language_code=language_code,
753
+ )
754
+
755
+ async def send_location_template(
756
+ self,
757
+ template_name: str,
758
+ recipient: str,
759
+ latitude: str,
760
+ longitude: str,
761
+ name: str,
762
+ address: str,
763
+ body_parameters: list[dict] | None = None,
764
+ language_code: str = "es",
765
+ ) -> MessageResult:
766
+ """Send template message with location header using WhatsApp API.
767
+
768
+ Supports templates with geographic location headers showing a map preview.
769
+ Coordinates must be valid latitude (-90 to 90) and longitude (-180 to 180).
770
+
771
+ Args:
772
+ template_name: Name of the approved WhatsApp template
773
+ recipient: Recipient phone number
774
+ latitude: Location latitude as string (e.g., "37.483307")
775
+ longitude: Location longitude as string (e.g., "-122.148981")
776
+ name: Name/title of the location
777
+ address: Physical address of the location
778
+ body_parameters: List of parameter objects for text replacement
779
+ language_code: BCP-47 language code for template (default: "es")
780
+
781
+ Returns:
782
+ MessageResult with operation status and metadata
783
+ """
784
+ # Convert Dict parameters to TemplateParameter objects if needed
785
+ template_parameters = None
786
+ if body_parameters:
787
+ from wappa.messaging.whatsapp.models.template_models import (
788
+ TemplateParameter,
789
+ TemplateParameterType,
790
+ )
791
+
792
+ template_parameters = []
793
+ for param in body_parameters:
794
+ if isinstance(param, dict) and param.get("type") == "text":
795
+ template_parameters.append(
796
+ TemplateParameter(
797
+ type=TemplateParameterType.TEXT, text=param.get("text")
798
+ )
799
+ )
800
+
801
+ return await self.template_handler.send_location_template(
802
+ phone_number=recipient,
803
+ template_name=template_name,
804
+ latitude=latitude,
805
+ longitude=longitude,
806
+ name=name,
807
+ address=address,
808
+ body_parameters=template_parameters,
809
+ language_code=language_code,
810
+ )
811
+
812
+ # Specialized Messaging Methods (from WhatsAppSpecializedHandler)
813
+
814
+ async def send_contact(
815
+ self, contact: dict, recipient: str, reply_to_message_id: str | None = None
816
+ ) -> MessageResult:
817
+ """Send contact card message using WhatsApp API.
818
+
819
+ Shares contact information including name, phone numbers, emails, and addresses.
820
+ Contact cards are automatically added to the recipient's address book.
821
+
822
+ Args:
823
+ contact: Contact information dictionary with required 'name' and 'phones' fields
824
+ recipient: Recipient phone number
825
+ reply_to_message_id: Optional message ID to reply to
826
+
827
+ Returns:
828
+ MessageResult with operation status and metadata
829
+ """
830
+ # Convert Dict to ContactCard model if needed
831
+ from wappa.messaging.whatsapp.models.specialized_models import ContactCard
832
+
833
+ if isinstance(contact, dict):
834
+ try:
835
+ contact_card = ContactCard(**contact)
836
+ except Exception as e:
837
+ return MessageResult(
838
+ success=False,
839
+ platform="whatsapp",
840
+ error=f"Invalid contact format: {str(e)}",
841
+ error_code="INVALID_CONTACT_FORMAT",
842
+ )
843
+ else:
844
+ contact_card = contact
845
+
846
+ return await self.specialized_handler.send_contact_card(
847
+ recipient=recipient,
848
+ contact=contact_card,
849
+ reply_to_message_id=reply_to_message_id,
850
+ )
851
+
852
+ async def send_location(
853
+ self,
854
+ latitude: float,
855
+ longitude: float,
856
+ recipient: str,
857
+ name: str | None = None,
858
+ address: str | None = None,
859
+ reply_to_message_id: str | None = None,
860
+ ) -> MessageResult:
861
+ """Send location message using WhatsApp API.
862
+
863
+ Shares geographic coordinates with optional location name and address.
864
+ Recipients see a map preview with the shared location.
865
+
866
+ Args:
867
+ latitude: Location latitude in decimal degrees (-90 to 90)
868
+ longitude: Location longitude in decimal degrees (-180 to 180)
869
+ recipient: Recipient phone number
870
+ name: Optional location name (e.g., "Coffee Shop")
871
+ address: Optional street address
872
+ reply_to_message_id: Optional message ID to reply to
873
+
874
+ Returns:
875
+ MessageResult with operation status and metadata
876
+ """
877
+ return await self.specialized_handler.send_location(
878
+ recipient=recipient,
879
+ latitude=latitude,
880
+ longitude=longitude,
881
+ name=name,
882
+ address=address,
883
+ reply_to_message_id=reply_to_message_id,
884
+ )
885
+
886
+ async def send_location_request(
887
+ self, body: str, recipient: str, reply_to_message_id: str | None = None
888
+ ) -> MessageResult:
889
+ """Send location request message using WhatsApp API.
890
+
891
+ Sends an interactive message that prompts the recipient to share their location.
892
+ Recipients see a "Send Location" button that allows easy location sharing.
893
+
894
+ Args:
895
+ body: Request message text (max 1024 characters)
896
+ recipient: Recipient phone number
897
+ reply_to_message_id: Optional message ID to reply to
898
+
899
+ Returns:
900
+ MessageResult with operation status and metadata
901
+ """
902
+ return await self.specialized_handler.send_location_request(
903
+ recipient=recipient, body=body, reply_to_message_id=reply_to_message_id
904
+ )