wappa 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wappa might be problematic. Click here for more details.
- wappa/__init__.py +85 -0
- wappa/api/__init__.py +1 -0
- wappa/api/controllers/__init__.py +10 -0
- wappa/api/controllers/webhook_controller.py +441 -0
- wappa/api/dependencies/__init__.py +15 -0
- wappa/api/dependencies/whatsapp_dependencies.py +220 -0
- wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
- wappa/api/middleware/__init__.py +7 -0
- wappa/api/middleware/error_handler.py +158 -0
- wappa/api/middleware/owner.py +99 -0
- wappa/api/middleware/request_logging.py +184 -0
- wappa/api/routes/__init__.py +6 -0
- wappa/api/routes/health.py +102 -0
- wappa/api/routes/webhooks.py +211 -0
- wappa/api/routes/whatsapp/__init__.py +15 -0
- wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
- wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
- wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
- wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
- wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
- wappa/api/routes/whatsapp_combined.py +35 -0
- wappa/cli/__init__.py +9 -0
- wappa/cli/main.py +199 -0
- wappa/core/__init__.py +6 -0
- wappa/core/config/__init__.py +5 -0
- wappa/core/config/settings.py +161 -0
- wappa/core/events/__init__.py +41 -0
- wappa/core/events/default_handlers.py +642 -0
- wappa/core/events/event_dispatcher.py +244 -0
- wappa/core/events/event_handler.py +247 -0
- wappa/core/events/webhook_factory.py +219 -0
- wappa/core/factory/__init__.py +15 -0
- wappa/core/factory/plugin.py +68 -0
- wappa/core/factory/wappa_builder.py +326 -0
- wappa/core/logging/__init__.py +5 -0
- wappa/core/logging/context.py +100 -0
- wappa/core/logging/logger.py +343 -0
- wappa/core/plugins/__init__.py +34 -0
- wappa/core/plugins/auth_plugin.py +169 -0
- wappa/core/plugins/cors_plugin.py +128 -0
- wappa/core/plugins/custom_middleware_plugin.py +182 -0
- wappa/core/plugins/database_plugin.py +235 -0
- wappa/core/plugins/rate_limit_plugin.py +183 -0
- wappa/core/plugins/redis_plugin.py +224 -0
- wappa/core/plugins/wappa_core_plugin.py +261 -0
- wappa/core/plugins/webhook_plugin.py +253 -0
- wappa/core/types.py +108 -0
- wappa/core/wappa_app.py +546 -0
- wappa/database/__init__.py +18 -0
- wappa/database/adapter.py +107 -0
- wappa/database/adapters/__init__.py +17 -0
- wappa/database/adapters/mysql_adapter.py +187 -0
- wappa/database/adapters/postgresql_adapter.py +169 -0
- wappa/database/adapters/sqlite_adapter.py +174 -0
- wappa/domain/__init__.py +28 -0
- wappa/domain/builders/__init__.py +5 -0
- wappa/domain/builders/message_builder.py +189 -0
- wappa/domain/entities/__init__.py +5 -0
- wappa/domain/enums/messenger_platform.py +123 -0
- wappa/domain/factories/__init__.py +6 -0
- wappa/domain/factories/media_factory.py +450 -0
- wappa/domain/factories/message_factory.py +497 -0
- wappa/domain/factories/messenger_factory.py +244 -0
- wappa/domain/interfaces/__init__.py +32 -0
- wappa/domain/interfaces/base_repository.py +94 -0
- wappa/domain/interfaces/cache_factory.py +85 -0
- wappa/domain/interfaces/cache_interface.py +199 -0
- wappa/domain/interfaces/expiry_repository.py +68 -0
- wappa/domain/interfaces/media_interface.py +311 -0
- wappa/domain/interfaces/messaging_interface.py +523 -0
- wappa/domain/interfaces/pubsub_repository.py +151 -0
- wappa/domain/interfaces/repository_factory.py +108 -0
- wappa/domain/interfaces/shared_state_repository.py +122 -0
- wappa/domain/interfaces/state_repository.py +123 -0
- wappa/domain/interfaces/tables_repository.py +215 -0
- wappa/domain/interfaces/user_repository.py +114 -0
- wappa/domain/interfaces/webhooks/__init__.py +1 -0
- wappa/domain/models/media_result.py +110 -0
- wappa/domain/models/platforms/__init__.py +15 -0
- wappa/domain/models/platforms/platform_config.py +104 -0
- wappa/domain/services/__init__.py +11 -0
- wappa/domain/services/tenant_credentials_service.py +56 -0
- wappa/messaging/__init__.py +7 -0
- wappa/messaging/whatsapp/__init__.py +1 -0
- wappa/messaging/whatsapp/client/__init__.py +5 -0
- wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
- wappa/messaging/whatsapp/handlers/__init__.py +13 -0
- wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
- wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
- wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
- wappa/messaging/whatsapp/messenger/__init__.py +5 -0
- wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
- wappa/messaging/whatsapp/models/__init__.py +61 -0
- wappa/messaging/whatsapp/models/basic_models.py +65 -0
- wappa/messaging/whatsapp/models/interactive_models.py +287 -0
- wappa/messaging/whatsapp/models/media_models.py +215 -0
- wappa/messaging/whatsapp/models/specialized_models.py +304 -0
- wappa/messaging/whatsapp/models/template_models.py +261 -0
- wappa/persistence/cache_factory.py +93 -0
- wappa/persistence/json/__init__.py +14 -0
- wappa/persistence/json/cache_adapters.py +271 -0
- wappa/persistence/json/handlers/__init__.py +1 -0
- wappa/persistence/json/handlers/state_handler.py +250 -0
- wappa/persistence/json/handlers/table_handler.py +263 -0
- wappa/persistence/json/handlers/user_handler.py +213 -0
- wappa/persistence/json/handlers/utils/__init__.py +1 -0
- wappa/persistence/json/handlers/utils/file_manager.py +153 -0
- wappa/persistence/json/handlers/utils/key_factory.py +11 -0
- wappa/persistence/json/handlers/utils/serialization.py +121 -0
- wappa/persistence/json/json_cache_factory.py +76 -0
- wappa/persistence/json/storage_manager.py +285 -0
- wappa/persistence/memory/__init__.py +14 -0
- wappa/persistence/memory/cache_adapters.py +271 -0
- wappa/persistence/memory/handlers/__init__.py +1 -0
- wappa/persistence/memory/handlers/state_handler.py +250 -0
- wappa/persistence/memory/handlers/table_handler.py +280 -0
- wappa/persistence/memory/handlers/user_handler.py +213 -0
- wappa/persistence/memory/handlers/utils/__init__.py +1 -0
- wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
- wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
- wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
- wappa/persistence/memory/memory_cache_factory.py +76 -0
- wappa/persistence/memory/storage_manager.py +235 -0
- wappa/persistence/redis/README.md +699 -0
- wappa/persistence/redis/__init__.py +11 -0
- wappa/persistence/redis/cache_adapters.py +285 -0
- wappa/persistence/redis/ops.py +880 -0
- wappa/persistence/redis/redis_cache_factory.py +71 -0
- wappa/persistence/redis/redis_client.py +231 -0
- wappa/persistence/redis/redis_handler/__init__.py +26 -0
- wappa/persistence/redis/redis_handler/state_handler.py +176 -0
- wappa/persistence/redis/redis_handler/table.py +158 -0
- wappa/persistence/redis/redis_handler/user.py +138 -0
- wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
- wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
- wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
- wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
- wappa/persistence/redis/redis_manager.py +189 -0
- wappa/processors/__init__.py +6 -0
- wappa/processors/base_processor.py +262 -0
- wappa/processors/factory.py +550 -0
- wappa/processors/whatsapp_processor.py +810 -0
- wappa/schemas/__init__.py +6 -0
- wappa/schemas/core/__init__.py +71 -0
- wappa/schemas/core/base_message.py +499 -0
- wappa/schemas/core/base_status.py +322 -0
- wappa/schemas/core/base_webhook.py +312 -0
- wappa/schemas/core/types.py +253 -0
- wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
- wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
- wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
- wappa/schemas/factory.py +754 -0
- wappa/schemas/webhooks/__init__.py +3 -0
- wappa/schemas/whatsapp/__init__.py +6 -0
- wappa/schemas/whatsapp/base_models.py +285 -0
- wappa/schemas/whatsapp/message_types/__init__.py +93 -0
- wappa/schemas/whatsapp/message_types/audio.py +350 -0
- wappa/schemas/whatsapp/message_types/button.py +267 -0
- wappa/schemas/whatsapp/message_types/contact.py +464 -0
- wappa/schemas/whatsapp/message_types/document.py +421 -0
- wappa/schemas/whatsapp/message_types/errors.py +195 -0
- wappa/schemas/whatsapp/message_types/image.py +424 -0
- wappa/schemas/whatsapp/message_types/interactive.py +430 -0
- wappa/schemas/whatsapp/message_types/location.py +416 -0
- wappa/schemas/whatsapp/message_types/order.py +372 -0
- wappa/schemas/whatsapp/message_types/reaction.py +271 -0
- wappa/schemas/whatsapp/message_types/sticker.py +328 -0
- wappa/schemas/whatsapp/message_types/system.py +317 -0
- wappa/schemas/whatsapp/message_types/text.py +411 -0
- wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
- wappa/schemas/whatsapp/message_types/video.py +344 -0
- wappa/schemas/whatsapp/status_models.py +479 -0
- wappa/schemas/whatsapp/validators.py +454 -0
- wappa/schemas/whatsapp/webhook_container.py +438 -0
- wappa/webhooks/__init__.py +17 -0
- wappa/webhooks/core/__init__.py +71 -0
- wappa/webhooks/core/base_message.py +499 -0
- wappa/webhooks/core/base_status.py +322 -0
- wappa/webhooks/core/base_webhook.py +312 -0
- wappa/webhooks/core/types.py +253 -0
- wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
- wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
- wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
- wappa/webhooks/factory.py +754 -0
- wappa/webhooks/whatsapp/__init__.py +6 -0
- wappa/webhooks/whatsapp/base_models.py +285 -0
- wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
- wappa/webhooks/whatsapp/message_types/audio.py +350 -0
- wappa/webhooks/whatsapp/message_types/button.py +267 -0
- wappa/webhooks/whatsapp/message_types/contact.py +464 -0
- wappa/webhooks/whatsapp/message_types/document.py +421 -0
- wappa/webhooks/whatsapp/message_types/errors.py +195 -0
- wappa/webhooks/whatsapp/message_types/image.py +424 -0
- wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
- wappa/webhooks/whatsapp/message_types/location.py +416 -0
- wappa/webhooks/whatsapp/message_types/order.py +372 -0
- wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
- wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
- wappa/webhooks/whatsapp/message_types/system.py +317 -0
- wappa/webhooks/whatsapp/message_types/text.py +411 -0
- wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
- wappa/webhooks/whatsapp/message_types/video.py +344 -0
- wappa/webhooks/whatsapp/status_models.py +479 -0
- wappa/webhooks/whatsapp/validators.py +454 -0
- wappa/webhooks/whatsapp/webhook_container.py +438 -0
- wappa-0.1.0.dist-info/METADATA +269 -0
- wappa-0.1.0.dist-info/RECORD +211 -0
- wappa-0.1.0.dist-info/WHEEL +4 -0
- wappa-0.1.0.dist-info/entry_points.txt +2 -0
- wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,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
|
+
)
|