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,440 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp media messaging API endpoints.
|
|
3
|
+
|
|
4
|
+
Provides REST API endpoints for WhatsApp media operations:
|
|
5
|
+
- POST /api/whatsapp/media/upload: Upload media files
|
|
6
|
+
- POST /api/whatsapp/media/send-image: Send image messages
|
|
7
|
+
- POST /api/whatsapp/media/send-video: Send video messages
|
|
8
|
+
- POST /api/whatsapp/media/send-audio: Send audio messages
|
|
9
|
+
- POST /api/whatsapp/media/send-document: Send document messages
|
|
10
|
+
- POST /api/whatsapp/media/send-sticker: Send sticker messages
|
|
11
|
+
- GET /api/whatsapp/media/info/{media_id}: Get media information
|
|
12
|
+
- GET /api/whatsapp/media/download/{media_id}: Download media
|
|
13
|
+
- DELETE /api/whatsapp/media/{media_id}: Delete media
|
|
14
|
+
|
|
15
|
+
Router configuration:
|
|
16
|
+
- Prefix: /whatsapp/media
|
|
17
|
+
- Tags: ["WhatsApp - Media"]
|
|
18
|
+
- Full URL: /api/whatsapp/media/ (when included with /api prefix)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
|
|
22
|
+
from fastapi.responses import StreamingResponse
|
|
23
|
+
|
|
24
|
+
from wappa.api.dependencies.whatsapp_dependencies import (
|
|
25
|
+
get_whatsapp_media_handler,
|
|
26
|
+
get_whatsapp_messenger,
|
|
27
|
+
)
|
|
28
|
+
from wappa.api.dependencies.whatsapp_media_dependencies import (
|
|
29
|
+
get_whatsapp_media_factory,
|
|
30
|
+
)
|
|
31
|
+
from wappa.domain.factories.media_factory import MediaFactory
|
|
32
|
+
from wappa.domain.interfaces.media_interface import IMediaHandler
|
|
33
|
+
from wappa.domain.interfaces.messaging_interface import IMessenger
|
|
34
|
+
from wappa.domain.models.media_result import (
|
|
35
|
+
MediaDeleteResult,
|
|
36
|
+
MediaInfoResult,
|
|
37
|
+
MediaUploadResult,
|
|
38
|
+
)
|
|
39
|
+
from wappa.messaging.whatsapp.models.basic_models import MessageResult
|
|
40
|
+
from wappa.messaging.whatsapp.models.media_models import (
|
|
41
|
+
AudioMessage,
|
|
42
|
+
DocumentMessage,
|
|
43
|
+
ImageMessage,
|
|
44
|
+
StickerMessage,
|
|
45
|
+
VideoMessage,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Create router with WhatsApp Media configuration
|
|
49
|
+
router = APIRouter(
|
|
50
|
+
prefix="/whatsapp/media",
|
|
51
|
+
tags=["WhatsApp - Media"],
|
|
52
|
+
responses={
|
|
53
|
+
400: {"description": "Bad Request - Invalid media format or size"},
|
|
54
|
+
401: {"description": "Unauthorized - Invalid tenant credentials"},
|
|
55
|
+
404: {"description": "Not Found - Media not found"},
|
|
56
|
+
413: {"description": "Payload Too Large - Media file too large"},
|
|
57
|
+
415: {"description": "Unsupported Media Type - Invalid file type"},
|
|
58
|
+
429: {"description": "Rate Limited - Too many requests"},
|
|
59
|
+
500: {"description": "Internal Server Error"},
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.post(
|
|
65
|
+
"/upload",
|
|
66
|
+
response_model=MediaUploadResult,
|
|
67
|
+
summary="Upload Media",
|
|
68
|
+
description="Upload a media file to WhatsApp servers and get media ID for sending",
|
|
69
|
+
)
|
|
70
|
+
async def upload_media(
|
|
71
|
+
file: UploadFile = File(..., description="Media file to upload"),
|
|
72
|
+
media_type: str | None = Form(
|
|
73
|
+
None, description="MIME type (auto-detected if not provided)"
|
|
74
|
+
),
|
|
75
|
+
media_handler: IMediaHandler = Depends(get_whatsapp_media_handler),
|
|
76
|
+
) -> MediaUploadResult:
|
|
77
|
+
"""Upload media file to WhatsApp servers.
|
|
78
|
+
|
|
79
|
+
Based on existing WhatsAppServiceMedia.upload_media() functionality.
|
|
80
|
+
Supports all WhatsApp media types with proper validation.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
# Read file data
|
|
84
|
+
file_data = await file.read()
|
|
85
|
+
|
|
86
|
+
# Use provided MIME type or file's content type
|
|
87
|
+
content_type = media_type or file.content_type
|
|
88
|
+
|
|
89
|
+
if not content_type:
|
|
90
|
+
raise HTTPException(
|
|
91
|
+
status_code=400,
|
|
92
|
+
detail="Could not determine media type. Please provide media_type parameter.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Upload using media handler
|
|
96
|
+
result = await media_handler.upload_media_from_bytes(
|
|
97
|
+
file_data=file_data,
|
|
98
|
+
media_type=content_type,
|
|
99
|
+
filename=file.filename or "uploaded_file",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if not result.success:
|
|
103
|
+
if result.error_code == "MIME_TYPE_UNSUPPORTED":
|
|
104
|
+
raise HTTPException(status_code=415, detail=result.error)
|
|
105
|
+
elif result.error_code == "FILE_SIZE_EXCEEDED":
|
|
106
|
+
raise HTTPException(status_code=413, detail=result.error)
|
|
107
|
+
else:
|
|
108
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
109
|
+
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
except HTTPException:
|
|
113
|
+
raise
|
|
114
|
+
except Exception as e:
|
|
115
|
+
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@router.post(
|
|
119
|
+
"/send-image",
|
|
120
|
+
response_model=MessageResult,
|
|
121
|
+
summary="Send Image Message",
|
|
122
|
+
description="Send an image message with optional caption and reply context",
|
|
123
|
+
)
|
|
124
|
+
async def send_image_message(
|
|
125
|
+
request: ImageMessage, messenger: IMessenger = Depends(get_whatsapp_messenger)
|
|
126
|
+
) -> MessageResult:
|
|
127
|
+
"""Send image message via WhatsApp.
|
|
128
|
+
|
|
129
|
+
Supports JPEG and PNG images up to 5MB with optional captions.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
result = await messenger.send_image(
|
|
133
|
+
image_source=request.media_source,
|
|
134
|
+
recipient=request.recipient,
|
|
135
|
+
caption=request.caption,
|
|
136
|
+
reply_to_message_id=request.reply_to_message_id,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if not result.success:
|
|
140
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
except HTTPException:
|
|
145
|
+
raise
|
|
146
|
+
except Exception as e:
|
|
147
|
+
raise HTTPException(status_code=500, detail=f"Failed to send image: {str(e)}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@router.post(
|
|
151
|
+
"/send-video",
|
|
152
|
+
response_model=MessageResult,
|
|
153
|
+
summary="Send Video Message",
|
|
154
|
+
description="Send a video message with optional caption and reply context",
|
|
155
|
+
)
|
|
156
|
+
async def send_video_message(
|
|
157
|
+
request: VideoMessage, messenger: IMessenger = Depends(get_whatsapp_messenger)
|
|
158
|
+
) -> MessageResult:
|
|
159
|
+
"""Send video message via WhatsApp.
|
|
160
|
+
|
|
161
|
+
Supports MP4 and 3GP videos up to 16MB with optional captions.
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
result = await messenger.send_video(
|
|
165
|
+
video_source=request.media_source,
|
|
166
|
+
recipient=request.recipient,
|
|
167
|
+
caption=request.caption,
|
|
168
|
+
reply_to_message_id=request.reply_to_message_id,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if not result.success:
|
|
172
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
173
|
+
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
except HTTPException:
|
|
177
|
+
raise
|
|
178
|
+
except Exception as e:
|
|
179
|
+
raise HTTPException(status_code=500, detail=f"Failed to send video: {str(e)}")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@router.post(
|
|
183
|
+
"/send-audio",
|
|
184
|
+
response_model=MessageResult,
|
|
185
|
+
summary="Send Audio Message",
|
|
186
|
+
description="Send an audio message with optional reply context",
|
|
187
|
+
)
|
|
188
|
+
async def send_audio_message(
|
|
189
|
+
request: AudioMessage, messenger: IMessenger = Depends(get_whatsapp_messenger)
|
|
190
|
+
) -> MessageResult:
|
|
191
|
+
"""Send audio message via WhatsApp.
|
|
192
|
+
|
|
193
|
+
Supports AAC, AMR, MP3, M4A, and OGG audio up to 16MB.
|
|
194
|
+
Note: Audio messages do not support captions.
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
result = await messenger.send_audio(
|
|
198
|
+
audio_source=request.media_source,
|
|
199
|
+
recipient=request.recipient,
|
|
200
|
+
reply_to_message_id=request.reply_to_message_id,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if not result.success:
|
|
204
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
205
|
+
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
except HTTPException:
|
|
209
|
+
raise
|
|
210
|
+
except Exception as e:
|
|
211
|
+
raise HTTPException(status_code=500, detail=f"Failed to send audio: {str(e)}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@router.post(
|
|
215
|
+
"/send-document",
|
|
216
|
+
response_model=MessageResult,
|
|
217
|
+
summary="Send Document Message",
|
|
218
|
+
description="Send a document message with optional filename and reply context",
|
|
219
|
+
)
|
|
220
|
+
async def send_document_message(
|
|
221
|
+
request: DocumentMessage, messenger: IMessenger = Depends(get_whatsapp_messenger)
|
|
222
|
+
) -> MessageResult:
|
|
223
|
+
"""Send document message via WhatsApp.
|
|
224
|
+
|
|
225
|
+
Supports TXT, PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX up to 100MB.
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
result = await messenger.send_document(
|
|
229
|
+
document_source=request.media_source,
|
|
230
|
+
recipient=request.recipient,
|
|
231
|
+
filename=request.filename,
|
|
232
|
+
reply_to_message_id=request.reply_to_message_id,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if not result.success:
|
|
236
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
237
|
+
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
except HTTPException:
|
|
241
|
+
raise
|
|
242
|
+
except Exception as e:
|
|
243
|
+
raise HTTPException(
|
|
244
|
+
status_code=500, detail=f"Failed to send document: {str(e)}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@router.post(
|
|
249
|
+
"/send-sticker",
|
|
250
|
+
response_model=MessageResult,
|
|
251
|
+
summary="Send Sticker Message",
|
|
252
|
+
description="Send a sticker message with optional reply context",
|
|
253
|
+
)
|
|
254
|
+
async def send_sticker_message(
|
|
255
|
+
request: StickerMessage, messenger: IMessenger = Depends(get_whatsapp_messenger)
|
|
256
|
+
) -> MessageResult:
|
|
257
|
+
"""Send sticker message via WhatsApp.
|
|
258
|
+
|
|
259
|
+
Supports WebP images only (100KB static, 500KB animated).
|
|
260
|
+
Note: Sticker messages do not support captions.
|
|
261
|
+
"""
|
|
262
|
+
try:
|
|
263
|
+
result = await messenger.send_sticker(
|
|
264
|
+
sticker_source=request.media_source,
|
|
265
|
+
recipient=request.recipient,
|
|
266
|
+
reply_to_message_id=request.reply_to_message_id,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if not result.success:
|
|
270
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
271
|
+
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
except HTTPException:
|
|
275
|
+
raise
|
|
276
|
+
except Exception as e:
|
|
277
|
+
raise HTTPException(status_code=500, detail=f"Failed to send sticker: {str(e)}")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@router.get(
|
|
281
|
+
"/info/{media_id}",
|
|
282
|
+
response_model=MediaInfoResult,
|
|
283
|
+
summary="Get Media Information",
|
|
284
|
+
description="Retrieve media information including URL, MIME type, and file size",
|
|
285
|
+
)
|
|
286
|
+
async def get_media_info(
|
|
287
|
+
media_id: str, media_handler: IMediaHandler = Depends(get_whatsapp_media_handler)
|
|
288
|
+
) -> MediaInfoResult:
|
|
289
|
+
"""Get media information by ID.
|
|
290
|
+
|
|
291
|
+
Returns media URL (valid for 5 minutes), MIME type, file size, and SHA256 hash.
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
result = await media_handler.get_media_info(media_id)
|
|
295
|
+
|
|
296
|
+
if not result.success:
|
|
297
|
+
if "not found" in result.error.lower():
|
|
298
|
+
raise HTTPException(status_code=404, detail=result.error)
|
|
299
|
+
else:
|
|
300
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
301
|
+
|
|
302
|
+
return result
|
|
303
|
+
|
|
304
|
+
except HTTPException:
|
|
305
|
+
raise
|
|
306
|
+
except Exception as e:
|
|
307
|
+
raise HTTPException(
|
|
308
|
+
status_code=500, detail=f"Failed to get media info: {str(e)}"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@router.get(
|
|
313
|
+
"/download/{media_id}",
|
|
314
|
+
summary="Download Media",
|
|
315
|
+
description="Download media file by ID as streaming response",
|
|
316
|
+
)
|
|
317
|
+
async def download_media(
|
|
318
|
+
media_id: str, media_handler: IMediaHandler = Depends(get_whatsapp_media_handler)
|
|
319
|
+
):
|
|
320
|
+
"""Download media by ID as streaming response.
|
|
321
|
+
|
|
322
|
+
Returns media file as streaming download with appropriate headers.
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
# Get media info first for headers
|
|
326
|
+
info_result = await media_handler.get_media_info(media_id)
|
|
327
|
+
if not info_result.success:
|
|
328
|
+
if "not found" in info_result.error.lower():
|
|
329
|
+
raise HTTPException(status_code=404, detail=info_result.error)
|
|
330
|
+
else:
|
|
331
|
+
raise HTTPException(status_code=400, detail=info_result.error)
|
|
332
|
+
|
|
333
|
+
# Download media
|
|
334
|
+
download_result = await media_handler.download_media(media_id)
|
|
335
|
+
if not download_result.success:
|
|
336
|
+
raise HTTPException(status_code=400, detail=download_result.error)
|
|
337
|
+
|
|
338
|
+
# Create streaming response
|
|
339
|
+
def generate_content():
|
|
340
|
+
yield download_result.file_data
|
|
341
|
+
|
|
342
|
+
# Determine filename
|
|
343
|
+
filename = f"media_{media_id}"
|
|
344
|
+
if download_result.mime_type:
|
|
345
|
+
# Simple extension mapping
|
|
346
|
+
ext_map = {
|
|
347
|
+
"image/jpeg": ".jpg",
|
|
348
|
+
"image/png": ".png",
|
|
349
|
+
"video/mp4": ".mp4",
|
|
350
|
+
"audio/mpeg": ".mp3",
|
|
351
|
+
"application/pdf": ".pdf",
|
|
352
|
+
}
|
|
353
|
+
if download_result.mime_type in ext_map:
|
|
354
|
+
filename += ext_map[download_result.mime_type]
|
|
355
|
+
|
|
356
|
+
return StreamingResponse(
|
|
357
|
+
generate_content(),
|
|
358
|
+
media_type=download_result.mime_type or "application/octet-stream",
|
|
359
|
+
headers={
|
|
360
|
+
"Content-Disposition": f"attachment; filename={filename}",
|
|
361
|
+
"Content-Length": str(download_result.file_size)
|
|
362
|
+
if download_result.file_size
|
|
363
|
+
else "",
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
except HTTPException:
|
|
368
|
+
raise
|
|
369
|
+
except Exception as e:
|
|
370
|
+
raise HTTPException(
|
|
371
|
+
status_code=500, detail=f"Failed to download media: {str(e)}"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@router.delete(
|
|
376
|
+
"/{media_id}",
|
|
377
|
+
response_model=MediaDeleteResult,
|
|
378
|
+
summary="Delete Media",
|
|
379
|
+
description="Delete media from WhatsApp servers",
|
|
380
|
+
)
|
|
381
|
+
async def delete_media(
|
|
382
|
+
media_id: str, media_handler: IMediaHandler = Depends(get_whatsapp_media_handler)
|
|
383
|
+
) -> MediaDeleteResult:
|
|
384
|
+
"""Delete media by ID.
|
|
385
|
+
|
|
386
|
+
Permanently removes media from WhatsApp servers.
|
|
387
|
+
Media files persist for 30 days unless deleted earlier.
|
|
388
|
+
"""
|
|
389
|
+
try:
|
|
390
|
+
result = await media_handler.delete_media(media_id)
|
|
391
|
+
|
|
392
|
+
if not result.success:
|
|
393
|
+
if "not found" in result.error.lower():
|
|
394
|
+
raise HTTPException(status_code=404, detail=result.error)
|
|
395
|
+
else:
|
|
396
|
+
raise HTTPException(status_code=400, detail=result.error)
|
|
397
|
+
|
|
398
|
+
return result
|
|
399
|
+
|
|
400
|
+
except HTTPException:
|
|
401
|
+
raise
|
|
402
|
+
except Exception as e:
|
|
403
|
+
raise HTTPException(status_code=500, detail=f"Failed to delete media: {str(e)}")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@router.get(
|
|
407
|
+
"/limits",
|
|
408
|
+
summary="Get Media Limits",
|
|
409
|
+
description="Get platform-specific media limits and supported types",
|
|
410
|
+
)
|
|
411
|
+
async def get_media_limits(
|
|
412
|
+
media_factory: MediaFactory = Depends(get_whatsapp_media_factory),
|
|
413
|
+
) -> dict:
|
|
414
|
+
"""Get WhatsApp media limits and constraints.
|
|
415
|
+
|
|
416
|
+
Returns supported MIME types, file size limits, and platform constraints.
|
|
417
|
+
"""
|
|
418
|
+
return media_factory.get_media_limits()
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@router.get(
|
|
422
|
+
"/health",
|
|
423
|
+
summary="Media Service Health Check",
|
|
424
|
+
description="Check health status of media services",
|
|
425
|
+
)
|
|
426
|
+
async def health_check(
|
|
427
|
+
media_handler: IMediaHandler = Depends(get_whatsapp_media_handler),
|
|
428
|
+
) -> dict:
|
|
429
|
+
"""Health check for media services.
|
|
430
|
+
|
|
431
|
+
Returns service status and configuration information.
|
|
432
|
+
"""
|
|
433
|
+
return {
|
|
434
|
+
"status": "healthy",
|
|
435
|
+
"service": "whatsapp-media",
|
|
436
|
+
"platform": media_handler.platform.value,
|
|
437
|
+
"tenant_id": media_handler.tenant_id,
|
|
438
|
+
"supported_types": len(media_handler.supported_media_types),
|
|
439
|
+
"max_file_sizes": media_handler.max_file_size,
|
|
440
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp messaging API endpoints.
|
|
3
|
+
|
|
4
|
+
Provides REST API endpoints for WhatsApp messaging operations:
|
|
5
|
+
- POST /api/whatsapp/messages/send-text: Send text messages
|
|
6
|
+
- POST /api/whatsapp/messages/mark-as-read: Mark messages as read with optional typing
|
|
7
|
+
|
|
8
|
+
Router configuration:
|
|
9
|
+
- Prefix: /whatsapp/messages
|
|
10
|
+
- Tags: ["WhatsApp - Messages"]
|
|
11
|
+
- Full URL: /api/whatsapp/messages/ (when included with /api prefix)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
15
|
+
|
|
16
|
+
from wappa.api.dependencies.whatsapp_dependencies import (
|
|
17
|
+
get_whatsapp_message_factory,
|
|
18
|
+
get_whatsapp_messenger,
|
|
19
|
+
)
|
|
20
|
+
from wappa.core.logging.logger import get_logger
|
|
21
|
+
from wappa.domain.factories.message_factory import MessageFactory
|
|
22
|
+
from wappa.domain.interfaces.messaging_interface import IMessenger
|
|
23
|
+
from wappa.messaging.whatsapp.models.basic_models import (
|
|
24
|
+
BasicTextMessage,
|
|
25
|
+
MessageResult,
|
|
26
|
+
ReadStatusMessage,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Create router with WhatsApp Messages configuration
|
|
30
|
+
router = APIRouter(
|
|
31
|
+
prefix="/whatsapp/messages",
|
|
32
|
+
tags=["WhatsApp - Messages"],
|
|
33
|
+
responses={
|
|
34
|
+
400: {"description": "Bad Request - Invalid message format"},
|
|
35
|
+
401: {"description": "Unauthorized - Invalid tenant credentials"},
|
|
36
|
+
429: {"description": "Rate Limited - Too many requests"},
|
|
37
|
+
500: {"description": "Internal Server Error"},
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.post(
|
|
43
|
+
"/send-text",
|
|
44
|
+
response_model=MessageResult,
|
|
45
|
+
summary="Send Text Message",
|
|
46
|
+
description="Send a text message via WhatsApp with optional reply and preview control",
|
|
47
|
+
)
|
|
48
|
+
async def send_text_message(
|
|
49
|
+
request: BasicTextMessage, messenger: IMessenger = Depends(get_whatsapp_messenger)
|
|
50
|
+
) -> MessageResult:
|
|
51
|
+
"""Send a text message via WhatsApp.
|
|
52
|
+
|
|
53
|
+
Sends a text message through WhatsApp Business API with support for:
|
|
54
|
+
- Reply to existing messages (threading)
|
|
55
|
+
- URL preview control
|
|
56
|
+
- Automatic URL detection and preview handling
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
request: Text message payload with recipient, content, and options
|
|
60
|
+
messenger: WhatsApp messenger implementation (injected)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
MessageResult with operation status, message ID, and metadata
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
HTTPException 400: If message validation fails
|
|
67
|
+
HTTPException 401: If tenant credentials are invalid
|
|
68
|
+
HTTPException 500: If WhatsApp API call fails
|
|
69
|
+
"""
|
|
70
|
+
logger = get_logger(__name__)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
logger.info(f"Sending text message to {request.recipient}")
|
|
74
|
+
|
|
75
|
+
result = await messenger.send_text(
|
|
76
|
+
text=request.text,
|
|
77
|
+
recipient=request.recipient,
|
|
78
|
+
reply_to_message_id=request.reply_to_message_id,
|
|
79
|
+
disable_preview=request.disable_preview,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if result.success:
|
|
83
|
+
logger.info(f"Text message sent successfully: {result.message_id}")
|
|
84
|
+
else:
|
|
85
|
+
logger.error(f"Failed to send text message: {result.error}")
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
except ValueError as e:
|
|
90
|
+
logger.error(f"Validation error sending text message: {str(e)}")
|
|
91
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f"Unexpected error sending text message: {str(e)}")
|
|
94
|
+
raise HTTPException(status_code=500, detail="Failed to send message")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@router.post(
|
|
98
|
+
"/mark-as-read",
|
|
99
|
+
response_model=MessageResult,
|
|
100
|
+
summary="Mark Message as Read",
|
|
101
|
+
description="Mark a WhatsApp message as read with optional typing indicator",
|
|
102
|
+
)
|
|
103
|
+
async def mark_message_as_read(
|
|
104
|
+
request: ReadStatusMessage, messenger: IMessenger = Depends(get_whatsapp_messenger)
|
|
105
|
+
) -> MessageResult:
|
|
106
|
+
"""Mark a WhatsApp message as read with optional typing indicator.
|
|
107
|
+
|
|
108
|
+
Marks a message as read through WhatsApp Business API with support for:
|
|
109
|
+
- Simple read receipt (typing=false)
|
|
110
|
+
- Read receipt with typing indicator (typing=true) - Key requirement
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
request: Read status payload with message ID and typing flag
|
|
114
|
+
messenger: WhatsApp messenger implementation (injected)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
MessageResult with operation status and metadata
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
HTTPException 400: If message ID is invalid
|
|
121
|
+
HTTPException 401: If tenant credentials are invalid
|
|
122
|
+
HTTPException 500: If WhatsApp API call fails
|
|
123
|
+
"""
|
|
124
|
+
logger = get_logger(__name__)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
action_desc = "with typing indicator" if request.typing else "without typing"
|
|
128
|
+
logger.info(f"Marking message {request.message_id} as read {action_desc}")
|
|
129
|
+
|
|
130
|
+
result = await messenger.mark_as_read(
|
|
131
|
+
message_id=request.message_id,
|
|
132
|
+
typing=request.typing, # Key requirement: typing boolean support
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if result.success:
|
|
136
|
+
logger.info(f"Message marked as read successfully: {request.message_id}")
|
|
137
|
+
else:
|
|
138
|
+
logger.error(f"Failed to mark message as read: {result.error}")
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
except ValueError as e:
|
|
143
|
+
logger.error(f"Validation error marking message as read: {str(e)}")
|
|
144
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error(f"Unexpected error marking message as read: {str(e)}")
|
|
147
|
+
raise HTTPException(status_code=500, detail="Failed to mark message as read")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@router.get(
|
|
151
|
+
"/limits",
|
|
152
|
+
summary="Get Message Limits",
|
|
153
|
+
description="Get WhatsApp platform-specific message limits and constraints",
|
|
154
|
+
)
|
|
155
|
+
async def get_message_limits(
|
|
156
|
+
factory: MessageFactory = Depends(get_whatsapp_message_factory),
|
|
157
|
+
) -> dict:
|
|
158
|
+
"""Get WhatsApp platform-specific message limits.
|
|
159
|
+
|
|
160
|
+
Returns current WhatsApp Business API limits for message validation
|
|
161
|
+
including text length limits, media size limits, and interactive message constraints.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
factory: WhatsApp message factory (injected)
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Dictionary containing platform-specific limits and constraints
|
|
168
|
+
"""
|
|
169
|
+
return factory.get_message_limits()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@router.get(
|
|
173
|
+
"/health",
|
|
174
|
+
summary="Health Check",
|
|
175
|
+
description="Check WhatsApp messaging service health",
|
|
176
|
+
)
|
|
177
|
+
async def health_check(messenger: IMessenger = Depends(get_whatsapp_messenger)) -> dict:
|
|
178
|
+
"""Health check for WhatsApp messaging service.
|
|
179
|
+
|
|
180
|
+
Verifies that the WhatsApp messaging service is properly configured
|
|
181
|
+
and ready to handle requests.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
messenger: WhatsApp messenger implementation (injected)
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Health status information
|
|
188
|
+
"""
|
|
189
|
+
return {
|
|
190
|
+
"status": "healthy",
|
|
191
|
+
"service": "whatsapp-messages",
|
|
192
|
+
"platform": messenger.platform.value,
|
|
193
|
+
"tenant_id": messenger.tenant_id,
|
|
194
|
+
"timestamp": "2025-01-26T00:00:00Z",
|
|
195
|
+
}
|