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.

Files changed (211) hide show
  1. wappa/__init__.py +85 -0
  2. wappa/api/__init__.py +1 -0
  3. wappa/api/controllers/__init__.py +10 -0
  4. wappa/api/controllers/webhook_controller.py +441 -0
  5. wappa/api/dependencies/__init__.py +15 -0
  6. wappa/api/dependencies/whatsapp_dependencies.py +220 -0
  7. wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
  8. wappa/api/middleware/__init__.py +7 -0
  9. wappa/api/middleware/error_handler.py +158 -0
  10. wappa/api/middleware/owner.py +99 -0
  11. wappa/api/middleware/request_logging.py +184 -0
  12. wappa/api/routes/__init__.py +6 -0
  13. wappa/api/routes/health.py +102 -0
  14. wappa/api/routes/webhooks.py +211 -0
  15. wappa/api/routes/whatsapp/__init__.py +15 -0
  16. wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
  17. wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
  18. wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
  19. wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
  20. wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
  21. wappa/api/routes/whatsapp_combined.py +35 -0
  22. wappa/cli/__init__.py +9 -0
  23. wappa/cli/main.py +199 -0
  24. wappa/core/__init__.py +6 -0
  25. wappa/core/config/__init__.py +5 -0
  26. wappa/core/config/settings.py +161 -0
  27. wappa/core/events/__init__.py +41 -0
  28. wappa/core/events/default_handlers.py +642 -0
  29. wappa/core/events/event_dispatcher.py +244 -0
  30. wappa/core/events/event_handler.py +247 -0
  31. wappa/core/events/webhook_factory.py +219 -0
  32. wappa/core/factory/__init__.py +15 -0
  33. wappa/core/factory/plugin.py +68 -0
  34. wappa/core/factory/wappa_builder.py +326 -0
  35. wappa/core/logging/__init__.py +5 -0
  36. wappa/core/logging/context.py +100 -0
  37. wappa/core/logging/logger.py +343 -0
  38. wappa/core/plugins/__init__.py +34 -0
  39. wappa/core/plugins/auth_plugin.py +169 -0
  40. wappa/core/plugins/cors_plugin.py +128 -0
  41. wappa/core/plugins/custom_middleware_plugin.py +182 -0
  42. wappa/core/plugins/database_plugin.py +235 -0
  43. wappa/core/plugins/rate_limit_plugin.py +183 -0
  44. wappa/core/plugins/redis_plugin.py +224 -0
  45. wappa/core/plugins/wappa_core_plugin.py +261 -0
  46. wappa/core/plugins/webhook_plugin.py +253 -0
  47. wappa/core/types.py +108 -0
  48. wappa/core/wappa_app.py +546 -0
  49. wappa/database/__init__.py +18 -0
  50. wappa/database/adapter.py +107 -0
  51. wappa/database/adapters/__init__.py +17 -0
  52. wappa/database/adapters/mysql_adapter.py +187 -0
  53. wappa/database/adapters/postgresql_adapter.py +169 -0
  54. wappa/database/adapters/sqlite_adapter.py +174 -0
  55. wappa/domain/__init__.py +28 -0
  56. wappa/domain/builders/__init__.py +5 -0
  57. wappa/domain/builders/message_builder.py +189 -0
  58. wappa/domain/entities/__init__.py +5 -0
  59. wappa/domain/enums/messenger_platform.py +123 -0
  60. wappa/domain/factories/__init__.py +6 -0
  61. wappa/domain/factories/media_factory.py +450 -0
  62. wappa/domain/factories/message_factory.py +497 -0
  63. wappa/domain/factories/messenger_factory.py +244 -0
  64. wappa/domain/interfaces/__init__.py +32 -0
  65. wappa/domain/interfaces/base_repository.py +94 -0
  66. wappa/domain/interfaces/cache_factory.py +85 -0
  67. wappa/domain/interfaces/cache_interface.py +199 -0
  68. wappa/domain/interfaces/expiry_repository.py +68 -0
  69. wappa/domain/interfaces/media_interface.py +311 -0
  70. wappa/domain/interfaces/messaging_interface.py +523 -0
  71. wappa/domain/interfaces/pubsub_repository.py +151 -0
  72. wappa/domain/interfaces/repository_factory.py +108 -0
  73. wappa/domain/interfaces/shared_state_repository.py +122 -0
  74. wappa/domain/interfaces/state_repository.py +123 -0
  75. wappa/domain/interfaces/tables_repository.py +215 -0
  76. wappa/domain/interfaces/user_repository.py +114 -0
  77. wappa/domain/interfaces/webhooks/__init__.py +1 -0
  78. wappa/domain/models/media_result.py +110 -0
  79. wappa/domain/models/platforms/__init__.py +15 -0
  80. wappa/domain/models/platforms/platform_config.py +104 -0
  81. wappa/domain/services/__init__.py +11 -0
  82. wappa/domain/services/tenant_credentials_service.py +56 -0
  83. wappa/messaging/__init__.py +7 -0
  84. wappa/messaging/whatsapp/__init__.py +1 -0
  85. wappa/messaging/whatsapp/client/__init__.py +5 -0
  86. wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
  87. wappa/messaging/whatsapp/handlers/__init__.py +13 -0
  88. wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
  89. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
  90. wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
  91. wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
  92. wappa/messaging/whatsapp/messenger/__init__.py +5 -0
  93. wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
  94. wappa/messaging/whatsapp/models/__init__.py +61 -0
  95. wappa/messaging/whatsapp/models/basic_models.py +65 -0
  96. wappa/messaging/whatsapp/models/interactive_models.py +287 -0
  97. wappa/messaging/whatsapp/models/media_models.py +215 -0
  98. wappa/messaging/whatsapp/models/specialized_models.py +304 -0
  99. wappa/messaging/whatsapp/models/template_models.py +261 -0
  100. wappa/persistence/cache_factory.py +93 -0
  101. wappa/persistence/json/__init__.py +14 -0
  102. wappa/persistence/json/cache_adapters.py +271 -0
  103. wappa/persistence/json/handlers/__init__.py +1 -0
  104. wappa/persistence/json/handlers/state_handler.py +250 -0
  105. wappa/persistence/json/handlers/table_handler.py +263 -0
  106. wappa/persistence/json/handlers/user_handler.py +213 -0
  107. wappa/persistence/json/handlers/utils/__init__.py +1 -0
  108. wappa/persistence/json/handlers/utils/file_manager.py +153 -0
  109. wappa/persistence/json/handlers/utils/key_factory.py +11 -0
  110. wappa/persistence/json/handlers/utils/serialization.py +121 -0
  111. wappa/persistence/json/json_cache_factory.py +76 -0
  112. wappa/persistence/json/storage_manager.py +285 -0
  113. wappa/persistence/memory/__init__.py +14 -0
  114. wappa/persistence/memory/cache_adapters.py +271 -0
  115. wappa/persistence/memory/handlers/__init__.py +1 -0
  116. wappa/persistence/memory/handlers/state_handler.py +250 -0
  117. wappa/persistence/memory/handlers/table_handler.py +280 -0
  118. wappa/persistence/memory/handlers/user_handler.py +213 -0
  119. wappa/persistence/memory/handlers/utils/__init__.py +1 -0
  120. wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
  121. wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
  122. wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
  123. wappa/persistence/memory/memory_cache_factory.py +76 -0
  124. wappa/persistence/memory/storage_manager.py +235 -0
  125. wappa/persistence/redis/README.md +699 -0
  126. wappa/persistence/redis/__init__.py +11 -0
  127. wappa/persistence/redis/cache_adapters.py +285 -0
  128. wappa/persistence/redis/ops.py +880 -0
  129. wappa/persistence/redis/redis_cache_factory.py +71 -0
  130. wappa/persistence/redis/redis_client.py +231 -0
  131. wappa/persistence/redis/redis_handler/__init__.py +26 -0
  132. wappa/persistence/redis/redis_handler/state_handler.py +176 -0
  133. wappa/persistence/redis/redis_handler/table.py +158 -0
  134. wappa/persistence/redis/redis_handler/user.py +138 -0
  135. wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
  136. wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
  137. wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
  138. wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
  139. wappa/persistence/redis/redis_manager.py +189 -0
  140. wappa/processors/__init__.py +6 -0
  141. wappa/processors/base_processor.py +262 -0
  142. wappa/processors/factory.py +550 -0
  143. wappa/processors/whatsapp_processor.py +810 -0
  144. wappa/schemas/__init__.py +6 -0
  145. wappa/schemas/core/__init__.py +71 -0
  146. wappa/schemas/core/base_message.py +499 -0
  147. wappa/schemas/core/base_status.py +322 -0
  148. wappa/schemas/core/base_webhook.py +312 -0
  149. wappa/schemas/core/types.py +253 -0
  150. wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
  151. wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
  152. wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
  153. wappa/schemas/factory.py +754 -0
  154. wappa/schemas/webhooks/__init__.py +3 -0
  155. wappa/schemas/whatsapp/__init__.py +6 -0
  156. wappa/schemas/whatsapp/base_models.py +285 -0
  157. wappa/schemas/whatsapp/message_types/__init__.py +93 -0
  158. wappa/schemas/whatsapp/message_types/audio.py +350 -0
  159. wappa/schemas/whatsapp/message_types/button.py +267 -0
  160. wappa/schemas/whatsapp/message_types/contact.py +464 -0
  161. wappa/schemas/whatsapp/message_types/document.py +421 -0
  162. wappa/schemas/whatsapp/message_types/errors.py +195 -0
  163. wappa/schemas/whatsapp/message_types/image.py +424 -0
  164. wappa/schemas/whatsapp/message_types/interactive.py +430 -0
  165. wappa/schemas/whatsapp/message_types/location.py +416 -0
  166. wappa/schemas/whatsapp/message_types/order.py +372 -0
  167. wappa/schemas/whatsapp/message_types/reaction.py +271 -0
  168. wappa/schemas/whatsapp/message_types/sticker.py +328 -0
  169. wappa/schemas/whatsapp/message_types/system.py +317 -0
  170. wappa/schemas/whatsapp/message_types/text.py +411 -0
  171. wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
  172. wappa/schemas/whatsapp/message_types/video.py +344 -0
  173. wappa/schemas/whatsapp/status_models.py +479 -0
  174. wappa/schemas/whatsapp/validators.py +454 -0
  175. wappa/schemas/whatsapp/webhook_container.py +438 -0
  176. wappa/webhooks/__init__.py +17 -0
  177. wappa/webhooks/core/__init__.py +71 -0
  178. wappa/webhooks/core/base_message.py +499 -0
  179. wappa/webhooks/core/base_status.py +322 -0
  180. wappa/webhooks/core/base_webhook.py +312 -0
  181. wappa/webhooks/core/types.py +253 -0
  182. wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
  183. wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
  184. wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
  185. wappa/webhooks/factory.py +754 -0
  186. wappa/webhooks/whatsapp/__init__.py +6 -0
  187. wappa/webhooks/whatsapp/base_models.py +285 -0
  188. wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
  189. wappa/webhooks/whatsapp/message_types/audio.py +350 -0
  190. wappa/webhooks/whatsapp/message_types/button.py +267 -0
  191. wappa/webhooks/whatsapp/message_types/contact.py +464 -0
  192. wappa/webhooks/whatsapp/message_types/document.py +421 -0
  193. wappa/webhooks/whatsapp/message_types/errors.py +195 -0
  194. wappa/webhooks/whatsapp/message_types/image.py +424 -0
  195. wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
  196. wappa/webhooks/whatsapp/message_types/location.py +416 -0
  197. wappa/webhooks/whatsapp/message_types/order.py +372 -0
  198. wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
  199. wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
  200. wappa/webhooks/whatsapp/message_types/system.py +317 -0
  201. wappa/webhooks/whatsapp/message_types/text.py +411 -0
  202. wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
  203. wappa/webhooks/whatsapp/message_types/video.py +344 -0
  204. wappa/webhooks/whatsapp/status_models.py +479 -0
  205. wappa/webhooks/whatsapp/validators.py +454 -0
  206. wappa/webhooks/whatsapp/webhook_container.py +438 -0
  207. wappa-0.1.0.dist-info/METADATA +269 -0
  208. wappa-0.1.0.dist-info/RECORD +211 -0
  209. wappa-0.1.0.dist-info/WHEEL +4 -0
  210. wappa-0.1.0.dist-info/entry_points.txt +2 -0
  211. 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
+ }