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