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