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,516 @@
1
+ """
2
+ WhatsApp Specialized Messaging API Routes.
3
+
4
+ Provides REST API endpoints for specialized WhatsApp messaging operations:
5
+ - Contact card sharing with comprehensive contact information
6
+ - Location sharing with coordinates, names, and addresses
7
+ - Location request messages with interactive prompts
8
+
9
+ Follows SOLID principles with proper error handling and Pydantic v2 validation.
10
+ Based on WhatsApp Cloud API 2025 specifications for specialized messaging.
11
+ """
12
+
13
+ import logging
14
+
15
+ from fastapi import APIRouter, Depends, HTTPException, status
16
+ from pydantic import BaseModel, Field, ValidationError
17
+
18
+ from wappa.api.dependencies.whatsapp_dependencies import get_whatsapp_messenger
19
+ from wappa.domain.interfaces.messaging_interface import IMessenger
20
+ from wappa.messaging.whatsapp.models.basic_models import MessageResult
21
+ from wappa.messaging.whatsapp.models.specialized_models import (
22
+ ContactCard,
23
+ ContactValidationResult,
24
+ LocationValidationResult,
25
+ )
26
+
27
+ # Configure route logger
28
+ logger = logging.getLogger(__name__)
29
+
30
+ router = APIRouter(
31
+ prefix="/specialized",
32
+ tags=["whatsapp - Specialized"],
33
+ responses={
34
+ 400: {"description": "Invalid request parameters"},
35
+ 401: {"description": "Authentication failed"},
36
+ 422: {"description": "Validation error"},
37
+ 500: {"description": "Internal server error"},
38
+ },
39
+ )
40
+
41
+
42
+ # Request Models
43
+ class ContactRequest(BaseModel):
44
+ """Request model for sending contact card messages."""
45
+
46
+ recipient: str = Field(..., description="Recipient phone number")
47
+ contact: ContactCard = Field(..., description="Contact information to share")
48
+ reply_to_message_id: str | None = Field(
49
+ None, description="Optional message ID to reply to"
50
+ )
51
+
52
+ model_config = {"extra": "forbid"}
53
+
54
+
55
+ class LocationRequest(BaseModel):
56
+ """Request model for sending location messages."""
57
+
58
+ recipient: str = Field(..., description="Recipient phone number")
59
+ latitude: float = Field(
60
+ ..., ge=-90, le=90, description="Location latitude in decimal degrees"
61
+ )
62
+ longitude: float = Field(
63
+ ..., ge=-180, le=180, description="Location longitude in decimal degrees"
64
+ )
65
+ name: str | None = Field(
66
+ None, max_length=1024, description="Optional location name"
67
+ )
68
+ address: str | None = Field(
69
+ None, max_length=1024, description="Optional street address"
70
+ )
71
+ reply_to_message_id: str | None = Field(
72
+ None, description="Optional message ID to reply to"
73
+ )
74
+
75
+ model_config = {"extra": "forbid"}
76
+
77
+
78
+ class LocationRequestRequest(BaseModel):
79
+ """Request model for sending location request messages."""
80
+
81
+ recipient: str = Field(..., description="Recipient phone number")
82
+ body: str = Field(
83
+ ..., min_length=1, max_length=1024, description="Request message text"
84
+ )
85
+ reply_to_message_id: str | None = Field(
86
+ None, description="Optional message ID to reply to"
87
+ )
88
+
89
+ model_config = {"extra": "forbid"}
90
+
91
+
92
+ class CoordinateValidationRequest(BaseModel):
93
+ """Request model for coordinate validation."""
94
+
95
+ latitude: float = Field(..., description="Latitude to validate")
96
+ longitude: float = Field(..., description="Longitude to validate")
97
+
98
+ model_config = {"extra": "forbid"}
99
+
100
+
101
+ # API Endpoints
102
+
103
+
104
+ @router.post("/send-contact", response_model=MessageResult)
105
+ async def send_contact_card(
106
+ request: ContactRequest, messenger: IMessenger = Depends(get_whatsapp_messenger)
107
+ ) -> MessageResult:
108
+ """
109
+ Send contact card message using WhatsApp API.
110
+
111
+ Shares contact information including name, phone numbers, emails, and addresses.
112
+ Contact cards are automatically added to the recipient's address book.
113
+
114
+ Args:
115
+ request: Contact card request with recipient and contact information
116
+ messenger: Injected WhatsApp messenger implementation
117
+
118
+ Returns:
119
+ MessageResult with operation status and metadata
120
+
121
+ Raises:
122
+ HTTPException: For validation errors, authentication failures, or API errors
123
+ """
124
+ try:
125
+ logger.info(f"Sending contact card to {request.recipient}")
126
+
127
+ # Convert ContactCard to dict for messenger interface
128
+ contact_dict = request.contact.model_dump()
129
+
130
+ result = await messenger.send_contact(
131
+ contact=contact_dict,
132
+ recipient=request.recipient,
133
+ reply_to_message_id=request.reply_to_message_id,
134
+ )
135
+
136
+ if not result.success:
137
+ # Map WhatsApp API errors to appropriate HTTP status codes
138
+ if "401" in str(result.error) or "Unauthorized" in str(result.error):
139
+ raise HTTPException(
140
+ status_code=status.HTTP_401_UNAUTHORIZED,
141
+ detail=f"WhatsApp authentication failed: {result.error}",
142
+ )
143
+ elif "400" in str(result.error) or "invalid" in str(result.error).lower():
144
+ raise HTTPException(
145
+ status_code=status.HTTP_400_BAD_REQUEST,
146
+ detail=f"Invalid contact data: {result.error}",
147
+ )
148
+ elif (
149
+ "429" in str(result.error) or "rate limit" in str(result.error).lower()
150
+ ):
151
+ raise HTTPException(
152
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
153
+ detail=f"Rate limit exceeded: {result.error}",
154
+ )
155
+ else:
156
+ raise HTTPException(
157
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
158
+ detail=f"Failed to send contact card: {result.error}",
159
+ )
160
+
161
+ logger.info(
162
+ f"Contact card sent successfully to {request.recipient}, message_id: {result.message_id}"
163
+ )
164
+ return result
165
+
166
+ except ValidationError as e:
167
+ logger.error(f"Contact validation error: {str(e)}")
168
+ raise HTTPException(
169
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
170
+ detail=f"Contact validation failed: {str(e)}",
171
+ )
172
+ except HTTPException:
173
+ raise
174
+ except Exception as e:
175
+ logger.error(f"Unexpected error sending contact card: {str(e)}")
176
+ raise HTTPException(
177
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
178
+ detail=f"Internal server error: {str(e)}",
179
+ )
180
+
181
+
182
+ @router.post("/send-location", response_model=MessageResult)
183
+ async def send_location_message(
184
+ request: LocationRequest, messenger: IMessenger = Depends(get_whatsapp_messenger)
185
+ ) -> MessageResult:
186
+ """
187
+ Send location message using WhatsApp API.
188
+
189
+ Shares geographic coordinates with optional location name and address.
190
+ Recipients see a map preview with the shared location.
191
+
192
+ Args:
193
+ request: Location request with coordinates and optional details
194
+ messenger: Injected WhatsApp messenger implementation
195
+
196
+ Returns:
197
+ MessageResult with operation status and metadata
198
+
199
+ Raises:
200
+ HTTPException: For validation errors, authentication failures, or API errors
201
+ """
202
+ try:
203
+ logger.info(
204
+ f"Sending location to {request.recipient}: ({request.latitude}, {request.longitude})"
205
+ )
206
+
207
+ result = await messenger.send_location(
208
+ latitude=request.latitude,
209
+ longitude=request.longitude,
210
+ recipient=request.recipient,
211
+ name=request.name,
212
+ address=request.address,
213
+ reply_to_message_id=request.reply_to_message_id,
214
+ )
215
+
216
+ if not result.success:
217
+ # Map WhatsApp API errors to appropriate HTTP status codes
218
+ if "401" in str(result.error) or "Unauthorized" in str(result.error):
219
+ raise HTTPException(
220
+ status_code=status.HTTP_401_UNAUTHORIZED,
221
+ detail=f"WhatsApp authentication failed: {result.error}",
222
+ )
223
+ elif "400" in str(result.error) or "invalid" in str(result.error).lower():
224
+ raise HTTPException(
225
+ status_code=status.HTTP_400_BAD_REQUEST,
226
+ detail=f"Invalid location data: {result.error}",
227
+ )
228
+ elif (
229
+ "429" in str(result.error) or "rate limit" in str(result.error).lower()
230
+ ):
231
+ raise HTTPException(
232
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
233
+ detail=f"Rate limit exceeded: {result.error}",
234
+ )
235
+ else:
236
+ raise HTTPException(
237
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
238
+ detail=f"Failed to send location: {result.error}",
239
+ )
240
+
241
+ logger.info(
242
+ f"Location sent successfully to {request.recipient}, message_id: {result.message_id}"
243
+ )
244
+ return result
245
+
246
+ except ValidationError as e:
247
+ logger.error(f"Location validation error: {str(e)}")
248
+ raise HTTPException(
249
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
250
+ detail=f"Location validation failed: {str(e)}",
251
+ )
252
+ except HTTPException:
253
+ raise
254
+ except Exception as e:
255
+ logger.error(f"Unexpected error sending location: {str(e)}")
256
+ raise HTTPException(
257
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
258
+ detail=f"Internal server error: {str(e)}",
259
+ )
260
+
261
+
262
+ @router.post("/send-location-request", response_model=MessageResult)
263
+ async def send_location_request_message(
264
+ request: LocationRequestRequest,
265
+ messenger: IMessenger = Depends(get_whatsapp_messenger),
266
+ ) -> MessageResult:
267
+ """
268
+ Send location request message using WhatsApp API.
269
+
270
+ Sends an interactive message that prompts the recipient to share their location.
271
+ Recipients see a "Send Location" button that allows easy location sharing.
272
+
273
+ Args:
274
+ request: Location request with message text
275
+ messenger: Injected WhatsApp messenger implementation
276
+
277
+ Returns:
278
+ MessageResult with operation status and metadata
279
+
280
+ Raises:
281
+ HTTPException: For validation errors, authentication failures, or API errors
282
+ """
283
+ try:
284
+ logger.info(f"Sending location request to {request.recipient}")
285
+
286
+ result = await messenger.send_location_request(
287
+ body=request.body,
288
+ recipient=request.recipient,
289
+ reply_to_message_id=request.reply_to_message_id,
290
+ )
291
+
292
+ if not result.success:
293
+ # Map WhatsApp API errors to appropriate HTTP status codes
294
+ if "401" in str(result.error) or "Unauthorized" in str(result.error):
295
+ raise HTTPException(
296
+ status_code=status.HTTP_401_UNAUTHORIZED,
297
+ detail=f"WhatsApp authentication failed: {result.error}",
298
+ )
299
+ elif "400" in str(result.error) or "invalid" in str(result.error).lower():
300
+ raise HTTPException(
301
+ status_code=status.HTTP_400_BAD_REQUEST,
302
+ detail=f"Invalid request data: {result.error}",
303
+ )
304
+ elif (
305
+ "429" in str(result.error) or "rate limit" in str(result.error).lower()
306
+ ):
307
+ raise HTTPException(
308
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
309
+ detail=f"Rate limit exceeded: {result.error}",
310
+ )
311
+ else:
312
+ raise HTTPException(
313
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
314
+ detail=f"Failed to send location request: {result.error}",
315
+ )
316
+
317
+ logger.info(
318
+ f"Location request sent successfully to {request.recipient}, message_id: {result.message_id}"
319
+ )
320
+ return result
321
+
322
+ except ValidationError as e:
323
+ logger.error(f"Location request validation error: {str(e)}")
324
+ raise HTTPException(
325
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
326
+ detail=f"Location request validation failed: {str(e)}",
327
+ )
328
+ except HTTPException:
329
+ raise
330
+ except Exception as e:
331
+ logger.error(f"Unexpected error sending location request: {str(e)}")
332
+ raise HTTPException(
333
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
334
+ detail=f"Internal server error: {str(e)}",
335
+ )
336
+
337
+
338
+ @router.post("/validate-contact", response_model=ContactValidationResult)
339
+ async def validate_contact_data(contact: ContactCard) -> ContactValidationResult:
340
+ """
341
+ Validate contact card data without sending a message.
342
+
343
+ Provides validation utilities for contact information to ensure compatibility
344
+ with WhatsApp Business API requirements before sending.
345
+
346
+ Args:
347
+ contact: Contact card data to validate
348
+
349
+ Returns:
350
+ ContactValidationResult with validation status and details
351
+
352
+ Raises:
353
+ HTTPException: For validation errors or processing failures
354
+ """
355
+ try:
356
+ logger.info("Validating contact card data")
357
+
358
+ # Pydantic v2 validation happens automatically on model instantiation
359
+ # Additional business logic validation can be added here
360
+
361
+ validation_issues = []
362
+
363
+ # Validate phone numbers format (basic validation)
364
+ for phone in contact.phones:
365
+ if not phone.phone.startswith("+"):
366
+ validation_issues.append(
367
+ f"Phone number should start with country code: {phone.phone}"
368
+ )
369
+
370
+ # Validate email format if provided
371
+ if contact.emails:
372
+ import re
373
+
374
+ email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
375
+ for email in contact.emails:
376
+ if not re.match(email_pattern, email.email):
377
+ validation_issues.append(f"Invalid email format: {email.email}")
378
+
379
+ is_valid = len(validation_issues) == 0
380
+
381
+ result = ContactValidationResult(
382
+ is_valid=is_valid,
383
+ validation_errors=validation_issues if validation_issues else None,
384
+ contact_summary=f"{contact.name.formatted_name} with {len(contact.phones)} phone(s)",
385
+ )
386
+
387
+ logger.info(
388
+ f"Contact validation completed: {'valid' if is_valid else 'invalid'}"
389
+ )
390
+ return result
391
+
392
+ except ValidationError as e:
393
+ logger.error(f"Contact validation error: {str(e)}")
394
+ raise HTTPException(
395
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
396
+ detail=f"Contact validation failed: {str(e)}",
397
+ )
398
+ except Exception as e:
399
+ logger.error(f"Unexpected error validating contact: {str(e)}")
400
+ raise HTTPException(
401
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
402
+ detail=f"Internal server error: {str(e)}",
403
+ )
404
+
405
+
406
+ @router.post("/validate-coordinates", response_model=LocationValidationResult)
407
+ async def validate_coordinates(
408
+ request: CoordinateValidationRequest,
409
+ ) -> LocationValidationResult:
410
+ """
411
+ Validate geographic coordinates without sending a message.
412
+
413
+ Provides validation utilities for latitude and longitude coordinates to ensure
414
+ they fall within valid ranges for location sharing.
415
+
416
+ Args:
417
+ request: Coordinates to validate
418
+
419
+ Returns:
420
+ LocationValidationResult with validation status and details
421
+
422
+ Raises:
423
+ HTTPException: For validation errors or processing failures
424
+ """
425
+ try:
426
+ logger.info(
427
+ f"Validating coordinates: ({request.latitude}, {request.longitude})"
428
+ )
429
+
430
+ validation_issues = []
431
+
432
+ # Validate latitude range
433
+ if not (-90 <= request.latitude <= 90):
434
+ validation_issues.append(
435
+ f"Latitude must be between -90 and 90 degrees: {request.latitude}"
436
+ )
437
+
438
+ # Validate longitude range
439
+ if not (-180 <= request.longitude <= 180):
440
+ validation_issues.append(
441
+ f"Longitude must be between -180 and 180 degrees: {request.longitude}"
442
+ )
443
+
444
+ # Check for null island (0,0) which might be unintentional
445
+ if request.latitude == 0 and request.longitude == 0:
446
+ validation_issues.append(
447
+ "Coordinates (0,0) point to Null Island - verify if intentional"
448
+ )
449
+
450
+ is_valid = len(validation_issues) == 0
451
+
452
+ # Determine location region for additional context
453
+ region = "Unknown"
454
+ if -90 <= request.latitude <= 90 and -180 <= request.longitude <= 180:
455
+ if request.latitude >= 0:
456
+ region = "Northern Hemisphere"
457
+ else:
458
+ region = "Southern Hemisphere"
459
+
460
+ result = LocationValidationResult(
461
+ is_valid=is_valid,
462
+ validation_errors=validation_issues if validation_issues else None,
463
+ coordinates_summary=f"({request.latitude}, {request.longitude}) - {region}",
464
+ )
465
+
466
+ logger.info(
467
+ f"Coordinate validation completed: {'valid' if is_valid else 'invalid'}"
468
+ )
469
+ return result
470
+
471
+ except ValidationError as e:
472
+ logger.error(f"Coordinate validation error: {str(e)}")
473
+ raise HTTPException(
474
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
475
+ detail=f"Coordinate validation failed: {str(e)}",
476
+ )
477
+ except Exception as e:
478
+ logger.error(f"Unexpected error validating coordinates: {str(e)}")
479
+ raise HTTPException(
480
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
481
+ detail=f"Internal server error: {str(e)}",
482
+ )
483
+
484
+
485
+ @router.get("/health")
486
+ async def specialized_service_health() -> dict:
487
+ """
488
+ Health check endpoint for specialized messaging service.
489
+
490
+ Provides service status and capability information for monitoring
491
+ and operational visibility.
492
+
493
+ Returns:
494
+ Service health status and capabilities
495
+ """
496
+ try:
497
+ # Basic health check - can be extended with actual service tests
498
+ return {
499
+ "status": "healthy",
500
+ "service": "whatsapp-specialized",
501
+ "capabilities": [
502
+ "contact_cards",
503
+ "location_sharing",
504
+ "location_requests",
505
+ "contact_validation",
506
+ "coordinate_validation",
507
+ ],
508
+ "api_version": "2025",
509
+ "whatsapp_api": "cloud_api",
510
+ }
511
+ except Exception as e:
512
+ logger.error(f"Health check failed: {str(e)}")
513
+ raise HTTPException(
514
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
515
+ detail=f"Service health check failed: {str(e)}",
516
+ )