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,431 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp template messaging API endpoints.
|
|
3
|
+
|
|
4
|
+
Provides REST API endpoints for WhatsApp Business template operations:
|
|
5
|
+
- POST /api/whatsapp/templates/send-text: Send text-only templates
|
|
6
|
+
- POST /api/whatsapp/templates/send-media: Send templates with media headers
|
|
7
|
+
- POST /api/whatsapp/templates/send-location: Send templates with location headers
|
|
8
|
+
- GET /api/whatsapp/templates/health: Service health check
|
|
9
|
+
|
|
10
|
+
Router configuration:
|
|
11
|
+
- Prefix: /whatsapp/templates
|
|
12
|
+
- Tags: ["WhatsApp - Templates"]
|
|
13
|
+
- Full URL: /api/whatsapp/templates/ (when included with /api prefix)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
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.template_models import (
|
|
22
|
+
LocationTemplateMessage,
|
|
23
|
+
MediaTemplateMessage,
|
|
24
|
+
TemplateMessageStatus,
|
|
25
|
+
TextTemplateMessage,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Create router with WhatsApp Templates configuration
|
|
29
|
+
router = APIRouter(
|
|
30
|
+
prefix="/whatsapp/templates",
|
|
31
|
+
tags=["WhatsApp - Templates"],
|
|
32
|
+
responses={
|
|
33
|
+
400: {"description": "Bad Request - Invalid template format or parameters"},
|
|
34
|
+
401: {"description": "Unauthorized - Invalid tenant credentials"},
|
|
35
|
+
403: {"description": "Forbidden - Template not approved or access denied"},
|
|
36
|
+
404: {"description": "Not Found - Template not found"},
|
|
37
|
+
413: {"description": "Payload Too Large - Template content too large"},
|
|
38
|
+
429: {"description": "Rate Limited - Too many requests"},
|
|
39
|
+
500: {"description": "Internal Server Error"},
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@router.post(
|
|
45
|
+
"/send-text",
|
|
46
|
+
response_model=MessageResult,
|
|
47
|
+
summary="Send Text Template Message",
|
|
48
|
+
description="Send a text-only template message with parameter substitution",
|
|
49
|
+
)
|
|
50
|
+
async def send_text_template(
|
|
51
|
+
request: TextTemplateMessage,
|
|
52
|
+
messenger: IMessenger = Depends(get_whatsapp_messenger),
|
|
53
|
+
) -> MessageResult:
|
|
54
|
+
"""Send text-only template message via WhatsApp.
|
|
55
|
+
|
|
56
|
+
Sends pre-approved business templates with dynamic parameter substitution.
|
|
57
|
+
Templates must be approved by WhatsApp before use.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
# Convert Pydantic model to dict format expected by messenger
|
|
61
|
+
body_parameters = None
|
|
62
|
+
if request.body_parameters:
|
|
63
|
+
body_parameters = []
|
|
64
|
+
for param in request.body_parameters:
|
|
65
|
+
body_parameters.append({"type": param.type.value, "text": param.text})
|
|
66
|
+
|
|
67
|
+
result = await messenger.send_text_template(
|
|
68
|
+
template_name=request.template_name,
|
|
69
|
+
recipient=request.recipient,
|
|
70
|
+
body_parameters=body_parameters,
|
|
71
|
+
language_code=request.language.code,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not result.success:
|
|
75
|
+
# Map specific template error codes to HTTP status codes
|
|
76
|
+
if result.error_code in ["TEMPLATE_NOT_FOUND", "TEMPLATE_NOT_APPROVED"]:
|
|
77
|
+
raise HTTPException(status_code=403, detail=result.error)
|
|
78
|
+
elif result.error_code in ["INVALID_PARAMETERS", "MISSING_PARAMETERS"]:
|
|
79
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
80
|
+
elif result.error_code in ["TEMPLATE_SEND_FAILED"]:
|
|
81
|
+
raise HTTPException(status_code=500, detail=result.error)
|
|
82
|
+
else:
|
|
83
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
84
|
+
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
except HTTPException:
|
|
88
|
+
raise
|
|
89
|
+
except Exception as e:
|
|
90
|
+
raise HTTPException(
|
|
91
|
+
status_code=500, detail=f"Failed to send text template: {str(e)}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@router.post(
|
|
96
|
+
"/send-media",
|
|
97
|
+
response_model=MessageResult,
|
|
98
|
+
summary="Send Media Template Message",
|
|
99
|
+
description="Send a template message with media header (image, video, or document)",
|
|
100
|
+
)
|
|
101
|
+
async def send_media_template(
|
|
102
|
+
request: MediaTemplateMessage,
|
|
103
|
+
messenger: IMessenger = Depends(get_whatsapp_messenger),
|
|
104
|
+
) -> MessageResult:
|
|
105
|
+
"""Send template message with media header via WhatsApp.
|
|
106
|
+
|
|
107
|
+
Supports templates with image, video, or document headers.
|
|
108
|
+
Either media_id (uploaded media) or media_url (external media) must be provided.
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
# Convert Pydantic model to dict format expected by messenger
|
|
112
|
+
body_parameters = None
|
|
113
|
+
if request.body_parameters:
|
|
114
|
+
body_parameters = []
|
|
115
|
+
for param in request.body_parameters:
|
|
116
|
+
body_parameters.append({"type": param.type.value, "text": param.text})
|
|
117
|
+
|
|
118
|
+
result = await messenger.send_media_template(
|
|
119
|
+
template_name=request.template_name,
|
|
120
|
+
recipient=request.recipient,
|
|
121
|
+
media_type=request.media_type.value,
|
|
122
|
+
media_id=request.media_id,
|
|
123
|
+
media_url=request.media_url,
|
|
124
|
+
body_parameters=body_parameters,
|
|
125
|
+
language_code=request.language.code,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if not result.success:
|
|
129
|
+
# Map specific template error codes to HTTP status codes
|
|
130
|
+
if result.error_code in ["TEMPLATE_NOT_FOUND", "TEMPLATE_NOT_APPROVED"]:
|
|
131
|
+
raise HTTPException(status_code=403, detail=result.error)
|
|
132
|
+
elif result.error_code in [
|
|
133
|
+
"INVALID_MEDIA_TYPE",
|
|
134
|
+
"MEDIA_NOT_FOUND",
|
|
135
|
+
"INVALID_PARAMETERS",
|
|
136
|
+
]:
|
|
137
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
138
|
+
elif result.error_code in ["MEDIA_TEMPLATE_SEND_FAILED"]:
|
|
139
|
+
raise HTTPException(status_code=500, detail=result.error)
|
|
140
|
+
else:
|
|
141
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
except HTTPException:
|
|
146
|
+
raise
|
|
147
|
+
except Exception as e:
|
|
148
|
+
raise HTTPException(
|
|
149
|
+
status_code=500, detail=f"Failed to send media template: {str(e)}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@router.post(
|
|
154
|
+
"/send-location",
|
|
155
|
+
response_model=MessageResult,
|
|
156
|
+
summary="Send Location Template Message",
|
|
157
|
+
description="Send a template message with location header and map preview",
|
|
158
|
+
)
|
|
159
|
+
async def send_location_template(
|
|
160
|
+
request: LocationTemplateMessage,
|
|
161
|
+
messenger: IMessenger = Depends(get_whatsapp_messenger),
|
|
162
|
+
) -> MessageResult:
|
|
163
|
+
"""Send template message with location header via WhatsApp.
|
|
164
|
+
|
|
165
|
+
Supports templates with geographic location headers showing a map preview.
|
|
166
|
+
Coordinates must be valid latitude (-90 to 90) and longitude (-180 to 180).
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
# Convert Pydantic model to dict format expected by messenger
|
|
170
|
+
body_parameters = None
|
|
171
|
+
if request.body_parameters:
|
|
172
|
+
body_parameters = []
|
|
173
|
+
for param in request.body_parameters:
|
|
174
|
+
body_parameters.append({"type": param.type.value, "text": param.text})
|
|
175
|
+
|
|
176
|
+
result = await messenger.send_location_template(
|
|
177
|
+
template_name=request.template_name,
|
|
178
|
+
recipient=request.recipient,
|
|
179
|
+
latitude=request.latitude,
|
|
180
|
+
longitude=request.longitude,
|
|
181
|
+
name=request.name,
|
|
182
|
+
address=request.address,
|
|
183
|
+
body_parameters=body_parameters,
|
|
184
|
+
language_code=request.language.code,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not result.success:
|
|
188
|
+
# Map specific template error codes to HTTP status codes
|
|
189
|
+
if result.error_code in ["TEMPLATE_NOT_FOUND", "TEMPLATE_NOT_APPROVED"]:
|
|
190
|
+
raise HTTPException(status_code=403, detail=result.error)
|
|
191
|
+
elif result.error_code in ["INVALID_COORDINATES", "INVALID_PARAMETERS"]:
|
|
192
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
193
|
+
elif result.error_code in ["LOCATION_TEMPLATE_SEND_FAILED"]:
|
|
194
|
+
raise HTTPException(status_code=500, detail=result.error)
|
|
195
|
+
else:
|
|
196
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
except HTTPException:
|
|
201
|
+
raise
|
|
202
|
+
except Exception as e:
|
|
203
|
+
raise HTTPException(
|
|
204
|
+
status_code=500, detail=f"Failed to send location template: {str(e)}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@router.get(
|
|
209
|
+
"/limits",
|
|
210
|
+
summary="Get Template Message Limits",
|
|
211
|
+
description="Get platform-specific template message limits and constraints",
|
|
212
|
+
)
|
|
213
|
+
async def get_template_limits() -> dict:
|
|
214
|
+
"""Get WhatsApp template message limits and constraints.
|
|
215
|
+
|
|
216
|
+
Returns supported template types, parameter limits, and platform constraints.
|
|
217
|
+
"""
|
|
218
|
+
return {
|
|
219
|
+
"text_templates": {
|
|
220
|
+
"max_body_parameters": 10,
|
|
221
|
+
"max_parameter_length": 1024,
|
|
222
|
+
"supported_parameter_types": ["text", "currency", "date_time"],
|
|
223
|
+
"supported_languages": [
|
|
224
|
+
"es",
|
|
225
|
+
"en",
|
|
226
|
+
"en_US",
|
|
227
|
+
"pt_BR",
|
|
228
|
+
"fr",
|
|
229
|
+
"de",
|
|
230
|
+
"it",
|
|
231
|
+
"ja",
|
|
232
|
+
"ko",
|
|
233
|
+
"zh",
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
"media_templates": {
|
|
237
|
+
"supported_media_types": ["image", "video", "document"],
|
|
238
|
+
"max_body_parameters": 10,
|
|
239
|
+
"media_requirements": {
|
|
240
|
+
"image": {"formats": ["JPEG", "PNG"], "max_size": "5MB"},
|
|
241
|
+
"video": {"formats": ["MP4", "3GP"], "max_size": "16MB"},
|
|
242
|
+
"document": {
|
|
243
|
+
"formats": ["PDF", "DOC", "DOCX", "XLS", "XLSX"],
|
|
244
|
+
"max_size": "100MB",
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
"location_templates": {
|
|
249
|
+
"coordinate_ranges": {
|
|
250
|
+
"latitude": {"min": -90, "max": 90},
|
|
251
|
+
"longitude": {"min": -180, "max": 180},
|
|
252
|
+
},
|
|
253
|
+
"max_name_length": 100,
|
|
254
|
+
"max_address_length": 1000,
|
|
255
|
+
"max_body_parameters": 10,
|
|
256
|
+
},
|
|
257
|
+
"general": {
|
|
258
|
+
"requires_approval": True,
|
|
259
|
+
"approval_process": "WhatsApp Business Account Manager",
|
|
260
|
+
"rate_limits": "Per WhatsApp Business API terms",
|
|
261
|
+
"supported_platforms": ["whatsapp"],
|
|
262
|
+
"requires_authentication": True,
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@router.get(
|
|
268
|
+
"/status/{template_name}",
|
|
269
|
+
response_model=TemplateMessageStatus,
|
|
270
|
+
summary="Get Template Status",
|
|
271
|
+
description="Get the approval status and configuration of a specific template",
|
|
272
|
+
)
|
|
273
|
+
async def get_template_status(
|
|
274
|
+
template_name: str,
|
|
275
|
+
language: str = "es",
|
|
276
|
+
messenger: IMessenger = Depends(get_whatsapp_messenger),
|
|
277
|
+
) -> TemplateMessageStatus:
|
|
278
|
+
"""Get template status and configuration.
|
|
279
|
+
|
|
280
|
+
Returns the approval status, category, and components of a WhatsApp template.
|
|
281
|
+
"""
|
|
282
|
+
try:
|
|
283
|
+
# This would typically call the template handler's get_template_info method
|
|
284
|
+
# For now, return a mock status structure
|
|
285
|
+
return TemplateMessageStatus(
|
|
286
|
+
template_name=template_name,
|
|
287
|
+
status="APPROVED", # This should come from actual API
|
|
288
|
+
language=language,
|
|
289
|
+
category="MARKETING", # This should come from actual API
|
|
290
|
+
components=[
|
|
291
|
+
{"type": "HEADER", "format": "TEXT"},
|
|
292
|
+
{"type": "BODY", "text": "Template body with parameters"},
|
|
293
|
+
{"type": "FOOTER", "text": "Optional footer text"},
|
|
294
|
+
],
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
except Exception as e:
|
|
298
|
+
raise HTTPException(
|
|
299
|
+
status_code=500, detail=f"Failed to get template status: {str(e)}"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@router.get(
|
|
304
|
+
"/health",
|
|
305
|
+
summary="Template Service Health Check",
|
|
306
|
+
description="Check health status of template messaging services",
|
|
307
|
+
)
|
|
308
|
+
async def health_check(messenger: IMessenger = Depends(get_whatsapp_messenger)) -> dict:
|
|
309
|
+
"""Health check for template messaging services.
|
|
310
|
+
|
|
311
|
+
Returns service status and configuration information.
|
|
312
|
+
"""
|
|
313
|
+
return {
|
|
314
|
+
"status": "healthy",
|
|
315
|
+
"service": "whatsapp-templates",
|
|
316
|
+
"platform": messenger.platform.value,
|
|
317
|
+
"tenant_id": messenger.tenant_id,
|
|
318
|
+
"template_types": ["text", "media", "location"],
|
|
319
|
+
"message_types_supported": [
|
|
320
|
+
"text",
|
|
321
|
+
"image",
|
|
322
|
+
"video",
|
|
323
|
+
"audio",
|
|
324
|
+
"document",
|
|
325
|
+
"sticker",
|
|
326
|
+
"button",
|
|
327
|
+
"list",
|
|
328
|
+
"cta_url",
|
|
329
|
+
"text_template",
|
|
330
|
+
"media_template",
|
|
331
|
+
"location_template",
|
|
332
|
+
],
|
|
333
|
+
"features": [
|
|
334
|
+
"Parameter substitution",
|
|
335
|
+
"Multi-language support",
|
|
336
|
+
"Media header templates",
|
|
337
|
+
"Location header templates",
|
|
338
|
+
"Template approval status",
|
|
339
|
+
],
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# Example endpoint demonstrating complex template usage
|
|
344
|
+
@router.post(
|
|
345
|
+
"/send-welcome-template",
|
|
346
|
+
response_model=MessageResult,
|
|
347
|
+
summary="Send Welcome Template (Example)",
|
|
348
|
+
description="Example endpoint showing complex template message with parameters",
|
|
349
|
+
)
|
|
350
|
+
async def send_welcome_template(
|
|
351
|
+
recipient: str,
|
|
352
|
+
customer_name: str,
|
|
353
|
+
business_name: str = "Mimeia Hotel",
|
|
354
|
+
messenger: IMessenger = Depends(get_whatsapp_messenger),
|
|
355
|
+
) -> MessageResult:
|
|
356
|
+
"""Example endpoint demonstrating welcome template with parameters.
|
|
357
|
+
|
|
358
|
+
This endpoint shows how to create a complex template message with
|
|
359
|
+
multiple parameters and proper error handling.
|
|
360
|
+
"""
|
|
361
|
+
try:
|
|
362
|
+
# Example welcome template parameters
|
|
363
|
+
body_parameters = [
|
|
364
|
+
{"type": "text", "text": customer_name},
|
|
365
|
+
{"type": "text", "text": business_name},
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
result = await messenger.send_text_template(
|
|
369
|
+
template_name="welcome_customer",
|
|
370
|
+
recipient=recipient,
|
|
371
|
+
body_parameters=body_parameters,
|
|
372
|
+
language_code="es",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
if not result.success:
|
|
376
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
377
|
+
|
|
378
|
+
return result
|
|
379
|
+
|
|
380
|
+
except HTTPException:
|
|
381
|
+
raise
|
|
382
|
+
except Exception as e:
|
|
383
|
+
raise HTTPException(
|
|
384
|
+
status_code=500, detail=f"Failed to send welcome template: {str(e)}"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# Example endpoint for media template with location
|
|
389
|
+
@router.post(
|
|
390
|
+
"/send-store-location-template",
|
|
391
|
+
response_model=MessageResult,
|
|
392
|
+
summary="Send Store Location Template (Example)",
|
|
393
|
+
description="Example endpoint showing location template for business use",
|
|
394
|
+
)
|
|
395
|
+
async def send_store_location_template(
|
|
396
|
+
recipient: str,
|
|
397
|
+
store_name: str = "Main Store",
|
|
398
|
+
messenger: IMessenger = Depends(get_whatsapp_messenger),
|
|
399
|
+
) -> MessageResult:
|
|
400
|
+
"""Example endpoint demonstrating store location template.
|
|
401
|
+
|
|
402
|
+
This endpoint shows how to create a location-based template for
|
|
403
|
+
business location sharing with customers.
|
|
404
|
+
"""
|
|
405
|
+
try:
|
|
406
|
+
# Example store location (replace with actual coordinates)
|
|
407
|
+
result = await messenger.send_location_template(
|
|
408
|
+
template_name="store_location",
|
|
409
|
+
recipient=recipient,
|
|
410
|
+
latitude="37.483307",
|
|
411
|
+
longitude="-122.148981",
|
|
412
|
+
name=store_name,
|
|
413
|
+
address="123 Business Ave, Business City, BC 12345",
|
|
414
|
+
body_parameters=[
|
|
415
|
+
{"type": "text", "text": store_name},
|
|
416
|
+
{"type": "text", "text": "Mon-Fri 9AM-6PM"},
|
|
417
|
+
],
|
|
418
|
+
language_code="es",
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
if not result.success:
|
|
422
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
423
|
+
|
|
424
|
+
return result
|
|
425
|
+
|
|
426
|
+
except HTTPException:
|
|
427
|
+
raise
|
|
428
|
+
except Exception as e:
|
|
429
|
+
raise HTTPException(
|
|
430
|
+
status_code=500, detail=f"Failed to send store location template: {str(e)}"
|
|
431
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Combined WhatsApp API router that includes all WhatsApp endpoints.
|
|
3
|
+
|
|
4
|
+
This module combines all WhatsApp API endpoints into a single router for easy inclusion
|
|
5
|
+
in the main Wappa application.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter
|
|
9
|
+
|
|
10
|
+
from .whatsapp import (
|
|
11
|
+
whatsapp_interactive_router,
|
|
12
|
+
whatsapp_media_router,
|
|
13
|
+
whatsapp_messages_router,
|
|
14
|
+
whatsapp_specialized_router,
|
|
15
|
+
whatsapp_templates_router,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Create a combined WhatsApp router
|
|
19
|
+
whatsapp_router = APIRouter(
|
|
20
|
+
prefix="/api/whatsapp",
|
|
21
|
+
tags=["WhatsApp API"],
|
|
22
|
+
responses={
|
|
23
|
+
400: {"description": "Bad Request - Invalid message format"},
|
|
24
|
+
401: {"description": "Unauthorized - Invalid tenant credentials"},
|
|
25
|
+
429: {"description": "Rate Limited - Too many requests"},
|
|
26
|
+
500: {"description": "Internal Server Error"},
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Include all WhatsApp sub-routers
|
|
31
|
+
whatsapp_router.include_router(whatsapp_messages_router)
|
|
32
|
+
whatsapp_router.include_router(whatsapp_media_router)
|
|
33
|
+
whatsapp_router.include_router(whatsapp_interactive_router)
|
|
34
|
+
whatsapp_router.include_router(whatsapp_templates_router)
|
|
35
|
+
whatsapp_router.include_router(whatsapp_specialized_router)
|
wappa/cli/__init__.py
ADDED
wappa/cli/main.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wappa CLI main module.
|
|
3
|
+
|
|
4
|
+
Provides clean command-line interface for development and production workflows.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Wappa WhatsApp Business Framework CLI")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _resolve_module_name(file_path: str) -> tuple[str, Path]:
|
|
18
|
+
"""
|
|
19
|
+
Convert a file path to a Python module name and working directory.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
main.py -> ("main", Path("."))
|
|
23
|
+
examples/redis_demo/main.py -> ("main", Path("examples/redis_demo"))
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
tuple[str, Path]: (module_name, working_directory)
|
|
27
|
+
"""
|
|
28
|
+
# Convert to Path object for better handling
|
|
29
|
+
path = Path(file_path)
|
|
30
|
+
|
|
31
|
+
# Get the directory and filename
|
|
32
|
+
working_dir = path.parent if path.parent != Path(".") else Path(".")
|
|
33
|
+
module_name = path.stem # Just the filename without extension
|
|
34
|
+
|
|
35
|
+
return module_name, working_dir
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command()
|
|
39
|
+
def dev(
|
|
40
|
+
file_path: str = typer.Argument(
|
|
41
|
+
..., help="Path to your Python file (e.g., main.py)"
|
|
42
|
+
),
|
|
43
|
+
app_var: str = typer.Option(
|
|
44
|
+
"app", "--app", "-a", help="Wappa instance variable name"
|
|
45
|
+
),
|
|
46
|
+
host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
|
|
47
|
+
port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"),
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Run development server with auto-reload.
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
wappa dev main.py
|
|
54
|
+
wappa dev examples/redis_demo/main.py --port 8080
|
|
55
|
+
wappa dev src/app.py --app my_wappa_app
|
|
56
|
+
"""
|
|
57
|
+
# Validate file exists
|
|
58
|
+
if not Path(file_path).exists():
|
|
59
|
+
typer.echo(f"❌ File not found: {file_path}", err=True)
|
|
60
|
+
raise typer.Exit(1)
|
|
61
|
+
|
|
62
|
+
# Convert file path to module name and working directory
|
|
63
|
+
module_name, working_dir = _resolve_module_name(file_path)
|
|
64
|
+
import_string = f"{module_name}:{app_var}.asgi"
|
|
65
|
+
|
|
66
|
+
# Build uvicorn command
|
|
67
|
+
cmd = [
|
|
68
|
+
sys.executable,
|
|
69
|
+
"-m",
|
|
70
|
+
"uvicorn",
|
|
71
|
+
import_string,
|
|
72
|
+
"--reload",
|
|
73
|
+
"--host",
|
|
74
|
+
host,
|
|
75
|
+
"--port",
|
|
76
|
+
str(port),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
typer.echo(f"🚀 Starting Wappa development server...")
|
|
80
|
+
typer.echo(f"📡 Import: {working_dir / module_name}:{app_var}.asgi")
|
|
81
|
+
typer.echo(f"🌐 Server: http://{host}:{port}")
|
|
82
|
+
typer.echo(f"📝 Docs: http://{host}:{port}/docs")
|
|
83
|
+
typer.echo("💡 Press CTRL+C to stop")
|
|
84
|
+
typer.echo()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
subprocess.run(cmd, check=True, cwd=working_dir)
|
|
88
|
+
except subprocess.CalledProcessError as e:
|
|
89
|
+
typer.echo(
|
|
90
|
+
f"❌ Development server failed to start (exit code: {e.returncode})",
|
|
91
|
+
err=True,
|
|
92
|
+
)
|
|
93
|
+
typer.echo("", err=True)
|
|
94
|
+
typer.echo("Common issues:", err=True)
|
|
95
|
+
typer.echo(f"• No module-level '{app_var}' variable in {file_path}", err=True)
|
|
96
|
+
typer.echo(
|
|
97
|
+
f"• Port {port} already in use (try --port with different number)", err=True
|
|
98
|
+
)
|
|
99
|
+
typer.echo(f"• Import errors in {file_path} or its dependencies", err=True)
|
|
100
|
+
typer.echo("", err=True)
|
|
101
|
+
typer.echo(
|
|
102
|
+
f"Make sure your file has: {app_var} = Wappa(...) at module level", err=True
|
|
103
|
+
)
|
|
104
|
+
raise typer.Exit(1)
|
|
105
|
+
except KeyboardInterrupt:
|
|
106
|
+
typer.echo("👋 Development server stopped")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.command()
|
|
110
|
+
def run(
|
|
111
|
+
file_path: str = typer.Argument(
|
|
112
|
+
..., help="Path to your Python file (e.g., main.py)"
|
|
113
|
+
),
|
|
114
|
+
app_var: str = typer.Option(
|
|
115
|
+
"app", "--app", "-a", help="Wappa instance variable name"
|
|
116
|
+
),
|
|
117
|
+
host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
|
|
118
|
+
port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"),
|
|
119
|
+
workers: int = typer.Option(
|
|
120
|
+
1, "--workers", "-w", help="Number of worker processes"
|
|
121
|
+
),
|
|
122
|
+
):
|
|
123
|
+
"""
|
|
124
|
+
Run production server (no auto-reload).
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
wappa run main.py
|
|
128
|
+
wappa run main.py --workers 4 --port 8080
|
|
129
|
+
"""
|
|
130
|
+
# Validate file exists
|
|
131
|
+
if not Path(file_path).exists():
|
|
132
|
+
typer.echo(f"❌ File not found: {file_path}", err=True)
|
|
133
|
+
raise typer.Exit(1)
|
|
134
|
+
|
|
135
|
+
# Convert file path to module name and working directory
|
|
136
|
+
module_name, working_dir = _resolve_module_name(file_path)
|
|
137
|
+
import_string = f"{module_name}:{app_var}.asgi"
|
|
138
|
+
|
|
139
|
+
# Build uvicorn command (no reload for production)
|
|
140
|
+
cmd = [
|
|
141
|
+
sys.executable,
|
|
142
|
+
"-m",
|
|
143
|
+
"uvicorn",
|
|
144
|
+
import_string,
|
|
145
|
+
"--host",
|
|
146
|
+
host,
|
|
147
|
+
"--port",
|
|
148
|
+
str(port),
|
|
149
|
+
"--workers",
|
|
150
|
+
str(workers),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
typer.echo(f"🚀 Starting Wappa production server...")
|
|
154
|
+
typer.echo(f"📡 Import: {working_dir / module_name}:{app_var}.asgi")
|
|
155
|
+
typer.echo(f"🌐 Server: http://{host}:{port}")
|
|
156
|
+
typer.echo(f"👥 Workers: {workers}")
|
|
157
|
+
typer.echo("💡 Press CTRL+C to stop")
|
|
158
|
+
typer.echo()
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
subprocess.run(cmd, check=True, cwd=working_dir)
|
|
162
|
+
except subprocess.CalledProcessError as e:
|
|
163
|
+
typer.echo(
|
|
164
|
+
f"❌ Production server failed to start (exit code: {e.returncode})",
|
|
165
|
+
err=True,
|
|
166
|
+
)
|
|
167
|
+
typer.echo("", err=True)
|
|
168
|
+
typer.echo("Common issues:", err=True)
|
|
169
|
+
typer.echo(f"• No module-level '{app_var}' variable in {file_path}", err=True)
|
|
170
|
+
typer.echo(f"• Port {port} already in use", err=True)
|
|
171
|
+
typer.echo(f"• Import errors in {file_path} or its dependencies", err=True)
|
|
172
|
+
raise typer.Exit(1)
|
|
173
|
+
except KeyboardInterrupt:
|
|
174
|
+
typer.echo("👋 Production server stopped")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.command()
|
|
178
|
+
def init(
|
|
179
|
+
project_name: str = typer.Argument(..., help="Project name"),
|
|
180
|
+
template: str = typer.Option(
|
|
181
|
+
"basic", "--template", "-t", help="Project template (basic, redis)"
|
|
182
|
+
),
|
|
183
|
+
):
|
|
184
|
+
"""
|
|
185
|
+
Initialize a new Wappa project (coming soon).
|
|
186
|
+
|
|
187
|
+
Examples:
|
|
188
|
+
wappa init my-whatsapp-bot
|
|
189
|
+
wappa init my-bot --template redis
|
|
190
|
+
"""
|
|
191
|
+
typer.echo("🚧 Project initialization coming soon!")
|
|
192
|
+
typer.echo(f"Project: {project_name}")
|
|
193
|
+
typer.echo(f"Template: {template}")
|
|
194
|
+
typer.echo()
|
|
195
|
+
typer.echo("For now, check out the examples directory for project templates.")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
app()
|
wappa/core/__init__.py
ADDED