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,434 @@
1
+ """
2
+ WhatsApp specialized message handler.
3
+
4
+ Provides specialized messaging operations using WhatsApp Cloud API:
5
+ - Contact card sharing
6
+ - Location sharing and requesting
7
+ - Coordinate validation and geocoding
8
+
9
+ Migrated from whatsapp_latest/services/special_messages.py with SOLID architecture.
10
+ """
11
+
12
+ from wappa.core.logging.logger import get_logger
13
+ from wappa.messaging.whatsapp.client.whatsapp_client import WhatsAppClient
14
+ from wappa.messaging.whatsapp.models.basic_models import MessageResult
15
+ from wappa.messaging.whatsapp.models.specialized_models import ContactCard
16
+
17
+
18
+ class WhatsAppSpecializedHandler:
19
+ """
20
+ Handler for WhatsApp specialized message operations.
21
+
22
+ Provides composition-based specialized functionality for WhatsAppMessenger:
23
+ - Contact card sharing with comprehensive contact information
24
+ - Location sharing with geographic coordinates and optional metadata
25
+ - Interactive location requests for user location sharing
26
+
27
+ Based on WhatsApp Cloud API 2025 specialized message specifications.
28
+ """
29
+
30
+ def __init__(self, client: WhatsAppClient, tenant_id: str):
31
+ """Initialize specialized handler.
32
+
33
+ Args:
34
+ client: Configured WhatsApp client for API operations
35
+ tenant_id: Tenant identifier for logging context
36
+ """
37
+ self.client = client
38
+ self._tenant_id = tenant_id
39
+ self.logger = get_logger(__name__)
40
+
41
+ async def send_contact_card(
42
+ self,
43
+ recipient: str,
44
+ contact: ContactCard,
45
+ reply_to_message_id: str | None = None,
46
+ ) -> MessageResult:
47
+ """
48
+ Send a contact card message via WhatsApp.
49
+
50
+ Args:
51
+ recipient: Recipient's phone number in E.164 format
52
+ contact: Contact card information
53
+ reply_to_message_id: Optional message ID to reply to
54
+
55
+ Returns:
56
+ MessageResult with operation status and metadata
57
+
58
+ Raises:
59
+ ValueError: If required contact fields are missing or invalid
60
+ Exception: For API request failures
61
+ """
62
+ try:
63
+ # Validate required contact fields
64
+ if not contact.name.formatted_name:
65
+ raise ValueError(
66
+ "Contact must include 'formatted_name' in the name object"
67
+ )
68
+
69
+ if not contact.phones or len(contact.phones) == 0:
70
+ raise ValueError("Contact must include at least one phone number")
71
+
72
+ # Convert ContactCard to WhatsApp API format
73
+ contact_dict = self._convert_contact_to_api_format(contact)
74
+
75
+ # Build message payload
76
+ payload = {
77
+ "messaging_product": "whatsapp",
78
+ "to": recipient,
79
+ "type": "contacts",
80
+ "contacts": [contact_dict],
81
+ }
82
+
83
+ # Add reply context if provided
84
+ if reply_to_message_id:
85
+ payload["context"] = {"message_id": reply_to_message_id}
86
+
87
+ self.logger.debug(
88
+ f"Sending contact card for '{contact.name.formatted_name}' to {recipient}"
89
+ )
90
+
91
+ # Send contact message
92
+ response = await self.client.post_request(payload)
93
+
94
+ # Parse response
95
+ if response.get("messages"):
96
+ message_id = response["messages"][0].get("id")
97
+ self.logger.info(f"Contact card sent successfully to {recipient}")
98
+
99
+ return MessageResult(
100
+ success=True,
101
+ message_id=message_id,
102
+ platform="whatsapp",
103
+ raw_response=response,
104
+ )
105
+ else:
106
+ error_msg = f"No message ID in response for contact card to {recipient}"
107
+ self.logger.error(error_msg)
108
+
109
+ return MessageResult(
110
+ success=False,
111
+ platform="whatsapp",
112
+ error=error_msg,
113
+ error_code="NO_MESSAGE_ID",
114
+ raw_response=response,
115
+ )
116
+
117
+ except Exception as e:
118
+ error_msg = f"Failed to send contact card to {recipient}: {str(e)}"
119
+ self.logger.exception(error_msg)
120
+
121
+ return MessageResult(
122
+ success=False,
123
+ platform="whatsapp",
124
+ error=error_msg,
125
+ error_code="CONTACT_SEND_FAILED",
126
+ )
127
+
128
+ async def send_location(
129
+ self,
130
+ recipient: str,
131
+ latitude: float,
132
+ longitude: float,
133
+ name: str | None = None,
134
+ address: str | None = None,
135
+ reply_to_message_id: str | None = None,
136
+ ) -> MessageResult:
137
+ """
138
+ Send a location message via WhatsApp.
139
+
140
+ Args:
141
+ recipient: Recipient's phone number in E.164 format
142
+ latitude: Location latitude in decimal degrees
143
+ longitude: Location longitude in decimal degrees
144
+ name: Optional location name (e.g., "Philz Coffee")
145
+ address: Optional location address
146
+ reply_to_message_id: Optional message ID to reply to
147
+
148
+ Returns:
149
+ MessageResult with operation status and metadata
150
+
151
+ Raises:
152
+ ValueError: If coordinates are invalid
153
+ Exception: For API request failures
154
+ """
155
+ try:
156
+ # Validate required parameters
157
+ if not recipient or latitude is None or longitude is None:
158
+ raise ValueError(
159
+ "recipient, latitude, and longitude are required parameters"
160
+ )
161
+
162
+ # Validate coordinate ranges
163
+ if not -90 <= latitude <= 90:
164
+ raise ValueError("Latitude must be between -90 and 90 degrees")
165
+
166
+ if not -180 <= longitude <= 180:
167
+ raise ValueError("Longitude must be between -180 and 180 degrees")
168
+
169
+ # Build location payload
170
+ location_data = {"latitude": str(latitude), "longitude": str(longitude)}
171
+
172
+ if name:
173
+ location_data["name"] = name
174
+ if address:
175
+ location_data["address"] = address
176
+
177
+ # Build message payload
178
+ payload = {
179
+ "messaging_product": "whatsapp",
180
+ "recipient_type": "individual",
181
+ "to": recipient,
182
+ "type": "location",
183
+ "location": location_data,
184
+ }
185
+
186
+ # Add reply context if provided
187
+ if reply_to_message_id:
188
+ payload["context"] = {"message_id": reply_to_message_id}
189
+
190
+ self.logger.debug(
191
+ f"Sending location ({latitude}, {longitude}) to {recipient}"
192
+ )
193
+
194
+ # Send location message
195
+ response = await self.client.post_request(payload)
196
+
197
+ # Parse response
198
+ if response.get("messages"):
199
+ message_id = response["messages"][0].get("id")
200
+ self.logger.info(f"Location message sent successfully to {recipient}")
201
+
202
+ return MessageResult(
203
+ success=True,
204
+ message_id=message_id,
205
+ platform="whatsapp",
206
+ raw_response=response,
207
+ )
208
+ else:
209
+ error_msg = (
210
+ f"No message ID in response for location message to {recipient}"
211
+ )
212
+ self.logger.error(error_msg)
213
+
214
+ return MessageResult(
215
+ success=False,
216
+ platform="whatsapp",
217
+ error=error_msg,
218
+ error_code="NO_MESSAGE_ID",
219
+ raw_response=response,
220
+ )
221
+
222
+ except Exception as e:
223
+ error_msg = f"Failed to send location to {recipient}: {str(e)}"
224
+ self.logger.exception(error_msg)
225
+
226
+ return MessageResult(
227
+ success=False,
228
+ platform="whatsapp",
229
+ error=error_msg,
230
+ error_code="LOCATION_SEND_FAILED",
231
+ )
232
+
233
+ async def send_location_request(
234
+ self, recipient: str, body: str, reply_to_message_id: str | None = None
235
+ ) -> MessageResult:
236
+ """
237
+ Send a location request message via WhatsApp.
238
+
239
+ This displays a message with a "Send Location" button that allows
240
+ users to share their location.
241
+
242
+ Args:
243
+ recipient: Recipient's phone number in E.164 format
244
+ body: Message text that appears above the location button (max 1024 chars)
245
+ reply_to_message_id: Optional message ID to reply to
246
+
247
+ Returns:
248
+ MessageResult with operation status and metadata
249
+
250
+ Raises:
251
+ ValueError: If required parameters are invalid
252
+ Exception: For API request failures
253
+ """
254
+ try:
255
+ # Validate required parameters
256
+ if not recipient or not body:
257
+ raise ValueError("recipient and body are required parameters")
258
+
259
+ # Validate body length
260
+ if len(body) > 1024:
261
+ raise ValueError("Body text cannot exceed 1024 characters")
262
+
263
+ # Build interactive location request payload
264
+ payload = {
265
+ "messaging_product": "whatsapp",
266
+ "recipient_type": "individual",
267
+ "type": "interactive",
268
+ "to": recipient,
269
+ "interactive": {
270
+ "type": "location_request_message",
271
+ "body": {"text": body},
272
+ "action": {"name": "send_location"},
273
+ },
274
+ }
275
+
276
+ # Add reply context if provided
277
+ if reply_to_message_id:
278
+ payload["context"] = {"message_id": reply_to_message_id}
279
+
280
+ self.logger.debug(f"Sending location request to {recipient}")
281
+
282
+ # Send location request message
283
+ response = await self.client.post_request(payload)
284
+
285
+ # Parse response
286
+ if response.get("messages"):
287
+ message_id = response["messages"][0].get("id")
288
+ self.logger.info(f"Location request sent successfully to {recipient}")
289
+
290
+ return MessageResult(
291
+ success=True,
292
+ message_id=message_id,
293
+ platform="whatsapp",
294
+ raw_response=response,
295
+ )
296
+ else:
297
+ error_msg = (
298
+ f"No message ID in response for location request to {recipient}"
299
+ )
300
+ self.logger.error(error_msg)
301
+
302
+ return MessageResult(
303
+ success=False,
304
+ platform="whatsapp",
305
+ error=error_msg,
306
+ error_code="NO_MESSAGE_ID",
307
+ raw_response=response,
308
+ )
309
+
310
+ except Exception as e:
311
+ error_msg = f"Failed to send location request to {recipient}: {str(e)}"
312
+ self.logger.exception(error_msg)
313
+
314
+ return MessageResult(
315
+ success=False,
316
+ platform="whatsapp",
317
+ error=error_msg,
318
+ error_code="LOCATION_REQUEST_FAILED",
319
+ )
320
+
321
+ def _convert_contact_to_api_format(self, contact: ContactCard) -> dict:
322
+ """
323
+ Convert ContactCard model to WhatsApp API contact format.
324
+
325
+ Args:
326
+ contact: ContactCard model instance
327
+
328
+ Returns:
329
+ Dict in WhatsApp API contact format
330
+ """
331
+ api_contact = {}
332
+
333
+ # Name (required)
334
+ api_contact["name"] = {"formatted_name": contact.name.formatted_name}
335
+
336
+ if contact.name.first_name:
337
+ api_contact["name"]["first_name"] = contact.name.first_name
338
+ if contact.name.last_name:
339
+ api_contact["name"]["last_name"] = contact.name.last_name
340
+ if contact.name.middle_name:
341
+ api_contact["name"]["middle_name"] = contact.name.middle_name
342
+ if contact.name.suffix:
343
+ api_contact["name"]["suffix"] = contact.name.suffix
344
+ if contact.name.prefix:
345
+ api_contact["name"]["prefix"] = contact.name.prefix
346
+
347
+ # Phones (required)
348
+ api_contact["phones"] = []
349
+ for phone in contact.phones:
350
+ phone_dict = {"phone": phone.phone, "type": phone.type.value}
351
+ if phone.wa_id:
352
+ phone_dict["wa_id"] = phone.wa_id
353
+ api_contact["phones"].append(phone_dict)
354
+
355
+ # Emails (optional)
356
+ if contact.emails:
357
+ api_contact["emails"] = []
358
+ for email in contact.emails:
359
+ api_contact["emails"].append(
360
+ {"email": email.email, "type": email.type.value}
361
+ )
362
+
363
+ # Addresses (optional)
364
+ if contact.addresses:
365
+ api_contact["addresses"] = []
366
+ for address in contact.addresses:
367
+ address_dict = {"type": address.type.value}
368
+ if address.street:
369
+ address_dict["street"] = address.street
370
+ if address.city:
371
+ address_dict["city"] = address.city
372
+ if address.state:
373
+ address_dict["state"] = address.state
374
+ if address.zip:
375
+ address_dict["zip"] = address.zip
376
+ if address.country:
377
+ address_dict["country"] = address.country
378
+ if address.country_code:
379
+ address_dict["country_code"] = address.country_code
380
+ api_contact["addresses"].append(address_dict)
381
+
382
+ # Organization (optional)
383
+ if contact.org:
384
+ api_contact["org"] = {}
385
+ if contact.org.company:
386
+ api_contact["org"]["company"] = contact.org.company
387
+ if contact.org.department:
388
+ api_contact["org"]["department"] = contact.org.department
389
+ if contact.org.title:
390
+ api_contact["org"]["title"] = contact.org.title
391
+
392
+ # URLs (optional)
393
+ if contact.urls:
394
+ api_contact["urls"] = []
395
+ for url in contact.urls:
396
+ api_contact["urls"].append({"url": url.url, "type": url.type.value})
397
+
398
+ # Birthday (optional)
399
+ if contact.birthday:
400
+ api_contact["birthday"] = contact.birthday
401
+
402
+ return api_contact
403
+
404
+ def validate_coordinates(self, latitude: float, longitude: float) -> dict:
405
+ """
406
+ Validate geographic coordinates.
407
+
408
+ Args:
409
+ latitude: Latitude coordinate
410
+ longitude: Longitude coordinate
411
+
412
+ Returns:
413
+ Dict with validation results and any errors
414
+ """
415
+ errors = []
416
+
417
+ # Validate latitude range
418
+ if not -90 <= latitude <= 90:
419
+ errors.append("Latitude must be between -90 and 90 degrees")
420
+
421
+ # Validate longitude range
422
+ if not -180 <= longitude <= 180:
423
+ errors.append("Longitude must be between -180 and 180 degrees")
424
+
425
+ # Check for obviously invalid coordinates (e.g., 0,0 unless intentional)
426
+ if latitude == 0 and longitude == 0:
427
+ errors.append("Coordinates (0,0) may be invalid - please verify location")
428
+
429
+ return {
430
+ "valid": len(errors) == 0,
431
+ "latitude": latitude,
432
+ "longitude": longitude,
433
+ "errors": errors if errors else None,
434
+ }