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,579 @@
1
+ """
2
+ WhatsApp implementation of the IMediaHandler interface.
3
+
4
+ Refactored from existing WhatsAppServiceMedia in whatsapp_latest/services/handle_media.py
5
+ to follow SOLID principles with dependency injection and proper separation of concerns.
6
+ """
7
+
8
+ import mimetypes
9
+ import time
10
+ from collections.abc import AsyncIterator
11
+ from pathlib import Path
12
+ from typing import Any, BinaryIO
13
+
14
+ from wappa.core.logging.logger import get_logger
15
+ from wappa.domain.interfaces.media_interface import IMediaHandler
16
+ from wappa.domain.models.media_result import (
17
+ MediaDeleteResult,
18
+ MediaDownloadResult,
19
+ MediaInfoResult,
20
+ MediaUploadResult,
21
+ )
22
+ from wappa.messaging.whatsapp.client.whatsapp_client import WhatsAppClient
23
+ from wappa.messaging.whatsapp.models.media_models import MediaType
24
+ from wappa.schemas.core.types import PlatformType
25
+
26
+
27
+ class WhatsAppMediaHandler(IMediaHandler):
28
+ """
29
+ WhatsApp implementation of the media handler interface.
30
+
31
+ Refactored from existing WhatsAppServiceMedia to follow SOLID principles:
32
+ - Single Responsibility: Only handles media operations
33
+ - Open/Closed: Extensible through interface implementation
34
+ - Dependency Inversion: Depends on WhatsAppClient abstraction
35
+
36
+ Based on WhatsApp Cloud API 2025 endpoints:
37
+ - POST /PHONE_NUMBER_ID/media (upload)
38
+ - GET /MEDIA_ID (get info/URL)
39
+ - DELETE /MEDIA_ID (delete)
40
+ - GET /MEDIA_URL (download)
41
+ """
42
+
43
+ def __init__(self, client: WhatsAppClient, tenant_id: str):
44
+ """Initialize WhatsApp media handler with client and tenant context.
45
+
46
+ Args:
47
+ client: Configured WhatsApp client for API operations
48
+ tenant_id: Tenant identifier (phone_number_id in WhatsApp context)
49
+ """
50
+ self.client = client
51
+ self._tenant_id = tenant_id
52
+ self.logger = get_logger(__name__)
53
+
54
+ @property
55
+ def platform(self) -> PlatformType:
56
+ """Get the platform this handler manages."""
57
+ return PlatformType.WHATSAPP
58
+
59
+ @property
60
+ def tenant_id(self) -> str:
61
+ """Get the tenant ID this handler serves."""
62
+ return self._tenant_id
63
+
64
+ @property
65
+ def supported_media_types(self) -> set[str]:
66
+ """Get supported MIME types for WhatsApp."""
67
+ supported_types = set()
68
+ for media_type in MediaType:
69
+ supported_types.update(MediaType.get_supported_mime_types(media_type))
70
+ return supported_types
71
+
72
+ @property
73
+ def max_file_size(self) -> dict[str, int]:
74
+ """Get maximum file sizes by media category."""
75
+ return {
76
+ "image": 5 * 1024 * 1024, # 5MB
77
+ "video": 16 * 1024 * 1024, # 16MB
78
+ "audio": 16 * 1024 * 1024, # 16MB
79
+ "document": 100 * 1024 * 1024, # 100MB
80
+ "sticker": 500 * 1024, # 500KB (animated), 100KB (static)
81
+ }
82
+
83
+ async def upload_media(
84
+ self,
85
+ file_path: str | Path,
86
+ media_type: str | None = None,
87
+ filename: str | None = None,
88
+ ) -> MediaUploadResult:
89
+ """
90
+ Upload media file to WhatsApp servers.
91
+
92
+ Based on existing WhatsAppServiceMedia.upload_media() method.
93
+ Implements POST /PHONE_NUMBER_ID/media endpoint.
94
+ """
95
+ try:
96
+ media_path = Path(file_path)
97
+ if not media_path.exists():
98
+ return MediaUploadResult(
99
+ success=False,
100
+ error=f"Media file not found: {media_path}",
101
+ error_code="FILE_NOT_FOUND",
102
+ tenant_id=self._tenant_id,
103
+ )
104
+
105
+ # Auto-detect MIME type if not provided
106
+ if media_type is None:
107
+ media_type = mimetypes.guess_type(media_path)[0]
108
+ if not media_type:
109
+ return MediaUploadResult(
110
+ success=False,
111
+ error=f"Could not determine MIME type for file: {media_path}",
112
+ error_code="MIME_TYPE_UNKNOWN",
113
+ tenant_id=self._tenant_id,
114
+ )
115
+
116
+ # Validate MIME type
117
+ if not self.validate_media_type(media_type):
118
+ return MediaUploadResult(
119
+ success=False,
120
+ error=f"Unsupported MIME type '{media_type}'. Supported types: {sorted(self.supported_media_types)}",
121
+ error_code="MIME_TYPE_UNSUPPORTED",
122
+ tenant_id=self._tenant_id,
123
+ )
124
+
125
+ # Validate file size
126
+ file_size = media_path.stat().st_size
127
+ if not self.validate_file_size(file_size, media_type):
128
+ max_size = self._get_max_size_for_mime_type(media_type)
129
+ return MediaUploadResult(
130
+ success=False,
131
+ error=f"File size ({file_size} bytes) exceeds the limit ({max_size} bytes) for type {media_type}",
132
+ error_code="FILE_SIZE_EXCEEDED",
133
+ tenant_id=self._tenant_id,
134
+ )
135
+
136
+ # Prepare upload data
137
+ data = {"messaging_product": "whatsapp", "type": media_type}
138
+
139
+ # Construct upload URL using client's URL builder
140
+ upload_url = self.client.url_builder.get_media_url()
141
+
142
+ self.logger.debug(f"Uploading media file {media_path.name} to {upload_url}")
143
+
144
+ with open(media_path, "rb") as file_handle:
145
+ files = {"file": (filename or media_path.name, file_handle, media_type)}
146
+
147
+ # Use the injected client for upload
148
+ result = await self.client.post_request(
149
+ payload=data, custom_url=upload_url, files=files
150
+ )
151
+
152
+ media_id = result.get("id")
153
+ if not media_id:
154
+ return MediaUploadResult(
155
+ success=False,
156
+ error=f"No media ID in response for {media_path.name}: {result}",
157
+ error_code="NO_MEDIA_ID",
158
+ tenant_id=self._tenant_id,
159
+ )
160
+
161
+ self.logger.info(
162
+ f"Successfully uploaded {media_path.name} (ID: {media_id})"
163
+ )
164
+ return MediaUploadResult(
165
+ success=True,
166
+ media_id=media_id,
167
+ file_size=file_size,
168
+ mime_type=media_type,
169
+ platform=PlatformType.WHATSAPP,
170
+ tenant_id=self._tenant_id,
171
+ )
172
+
173
+ except Exception as e:
174
+ self.logger.exception(f"Failed to upload {file_path}: {e}")
175
+ return MediaUploadResult(
176
+ success=False,
177
+ error=str(e),
178
+ error_code="UPLOAD_FAILED",
179
+ tenant_id=self._tenant_id,
180
+ )
181
+
182
+ async def upload_media_from_bytes(
183
+ self, file_data: bytes, media_type: str, filename: str
184
+ ) -> MediaUploadResult:
185
+ """Upload media from bytes data."""
186
+ try:
187
+ # Validate MIME type
188
+ if not self.validate_media_type(media_type):
189
+ return MediaUploadResult(
190
+ success=False,
191
+ error=f"Unsupported MIME type '{media_type}'. Supported types: {sorted(self.supported_media_types)}",
192
+ error_code="MIME_TYPE_UNSUPPORTED",
193
+ tenant_id=self._tenant_id,
194
+ )
195
+
196
+ # Validate file size
197
+ file_size = len(file_data)
198
+ if not self.validate_file_size(file_size, media_type):
199
+ max_size = self._get_max_size_for_mime_type(media_type)
200
+ return MediaUploadResult(
201
+ success=False,
202
+ error=f"File size ({file_size} bytes) exceeds the limit ({max_size} bytes) for type {media_type}",
203
+ error_code="FILE_SIZE_EXCEEDED",
204
+ tenant_id=self._tenant_id,
205
+ )
206
+
207
+ # Prepare upload data
208
+ data = {"messaging_product": "whatsapp", "type": media_type}
209
+
210
+ # Construct upload URL using client's URL builder
211
+ upload_url = self.client.url_builder.get_media_url()
212
+
213
+ self.logger.debug(f"Uploading media from bytes: {filename}")
214
+
215
+ files = {"file": (filename, file_data, media_type)}
216
+
217
+ result = await self.client.post_request(
218
+ payload=data, custom_url=upload_url, files=files
219
+ )
220
+
221
+ media_id = result.get("id")
222
+ if not media_id:
223
+ return MediaUploadResult(
224
+ success=False,
225
+ error=f"No media ID in response for {filename}: {result}",
226
+ error_code="NO_MEDIA_ID",
227
+ tenant_id=self._tenant_id,
228
+ )
229
+
230
+ self.logger.info(
231
+ f"Successfully uploaded {filename} from bytes (ID: {media_id})"
232
+ )
233
+ return MediaUploadResult(
234
+ success=True,
235
+ media_id=media_id,
236
+ file_size=file_size,
237
+ mime_type=media_type,
238
+ platform=PlatformType.WHATSAPP,
239
+ tenant_id=self._tenant_id,
240
+ )
241
+
242
+ except Exception as e:
243
+ self.logger.exception(f"Failed to upload {filename} from bytes: {e}")
244
+ return MediaUploadResult(
245
+ success=False,
246
+ error=str(e),
247
+ error_code="UPLOAD_FAILED",
248
+ tenant_id=self._tenant_id,
249
+ )
250
+
251
+ async def upload_media_from_stream(
252
+ self,
253
+ file_stream: BinaryIO,
254
+ media_type: str,
255
+ filename: str,
256
+ file_size: int | None = None,
257
+ ) -> MediaUploadResult:
258
+ """Upload media from file stream."""
259
+ try:
260
+ # Read stream data
261
+ file_data = file_stream.read()
262
+
263
+ # Use the bytes upload method
264
+ return await self.upload_media_from_bytes(file_data, media_type, filename)
265
+
266
+ except Exception as e:
267
+ self.logger.exception(f"Failed to upload {filename} from stream: {e}")
268
+ return MediaUploadResult(
269
+ success=False,
270
+ error=str(e),
271
+ error_code="UPLOAD_FAILED",
272
+ tenant_id=self._tenant_id,
273
+ )
274
+
275
+ async def get_media_info(self, media_id: str) -> MediaInfoResult:
276
+ """
277
+ Retrieve media information using media ID.
278
+
279
+ Based on existing WhatsAppServiceMedia.get_media_url() method.
280
+ Implements GET /MEDIA_ID endpoint.
281
+ """
282
+ try:
283
+ endpoint = f"{media_id}/"
284
+ self.logger.debug(f"Fetching media info for ID: {media_id}")
285
+
286
+ result = await self.client.get_request(endpoint=endpoint)
287
+
288
+ if not result or "url" not in result:
289
+ return MediaInfoResult(
290
+ success=False,
291
+ error=f"Invalid response for media ID {media_id}: {result}",
292
+ error_code="INVALID_RESPONSE",
293
+ tenant_id=self._tenant_id,
294
+ )
295
+
296
+ self.logger.info(f"Successfully retrieved media URL for ID: {media_id}")
297
+ return MediaInfoResult(
298
+ success=True,
299
+ media_id=media_id,
300
+ url=result.get("url"),
301
+ mime_type=result.get("mime_type"),
302
+ file_size=result.get("file_size"),
303
+ sha256=result.get("sha256"),
304
+ platform=PlatformType.WHATSAPP,
305
+ tenant_id=self._tenant_id,
306
+ )
307
+
308
+ except Exception as e:
309
+ self.logger.exception(f"Error getting info for media ID {media_id}: {e}")
310
+ return MediaInfoResult(
311
+ success=False,
312
+ error=str(e),
313
+ error_code="INFO_RETRIEVAL_FAILED",
314
+ tenant_id=self._tenant_id,
315
+ )
316
+
317
+ async def download_media(
318
+ self,
319
+ media_id: str,
320
+ destination_path: str | Path | None = None,
321
+ sender_id: str | None = None,
322
+ ) -> MediaDownloadResult:
323
+ """
324
+ Download WhatsApp media using its media ID.
325
+
326
+ Based on existing WhatsAppServiceMedia.download_media() method.
327
+ Implements workflow: GET /MEDIA_ID -> GET /MEDIA_URL
328
+ """
329
+ try:
330
+ # Get media info first
331
+ media_info_result = await self.get_media_info(media_id)
332
+ if not media_info_result.success:
333
+ return MediaDownloadResult(
334
+ success=False,
335
+ error=f"Failed to get media URL for ID {media_id}: {media_info_result.error}",
336
+ error_code="MEDIA_INFO_FAILED",
337
+ tenant_id=self._tenant_id,
338
+ )
339
+
340
+ media_url = media_info_result.url
341
+ content_type = media_info_result.mime_type
342
+
343
+ self.logger.debug(
344
+ f"Starting download for media ID: {media_id} from URL: {media_url}"
345
+ )
346
+
347
+ # Use the client for streaming request
348
+ session, response = await self.client.get_request_stream(media_url)
349
+
350
+ try:
351
+ if response.status != 200:
352
+ error_text = await response.text()
353
+ return MediaDownloadResult(
354
+ success=False,
355
+ error=f"Download failed for {media_id}: {response.status} - {error_text}",
356
+ error_code=f"HTTP_{response.status}",
357
+ tenant_id=self._tenant_id,
358
+ )
359
+
360
+ # Validate content type and size
361
+ response_content_type = response.headers.get(
362
+ "content-type", content_type
363
+ )
364
+ content_length_str = response.headers.get("content-length", "0")
365
+
366
+ try:
367
+ content_length = int(content_length_str)
368
+ except ValueError:
369
+ content_length = 0
370
+
371
+ # Validate against platform limits
372
+ if not self.validate_file_size(content_length, response_content_type):
373
+ max_size = self._get_max_size_for_mime_type(response_content_type)
374
+ return MediaDownloadResult(
375
+ success=False,
376
+ error=f"Media file size ({content_length} bytes) exceeds max allowed ({max_size} bytes) for type {response_content_type}",
377
+ error_code="FILE_SIZE_EXCEEDED",
378
+ tenant_id=self._tenant_id,
379
+ )
380
+
381
+ # Read response data
382
+ data = bytearray()
383
+ downloaded_size = 0
384
+ max_size = self._get_max_size_for_mime_type(response_content_type)
385
+
386
+ async for chunk in response.content.iter_chunked(8192):
387
+ if chunk:
388
+ downloaded_size += len(chunk)
389
+ if downloaded_size > max_size:
390
+ return MediaDownloadResult(
391
+ success=False,
392
+ error=f"Download aborted: file size ({downloaded_size}) exceeded max ({max_size}) bytes for type {response_content_type}",
393
+ error_code="FILE_SIZE_EXCEEDED",
394
+ tenant_id=self._tenant_id,
395
+ )
396
+ data.extend(chunk)
397
+
398
+ # Save to file if destination_path provided
399
+ final_path = None
400
+ if destination_path:
401
+ extension_map = self._get_extension_map()
402
+ extension = extension_map.get(response_content_type, "")
403
+ media_type_base = response_content_type.split("/")[0]
404
+ timestamp = int(time.time())
405
+ filename_final = f"{media_type_base}_{sender_id or 'unknown'}_{timestamp}{extension}"
406
+
407
+ path = Path(destination_path)
408
+ path.mkdir(parents=True, exist_ok=True)
409
+ final_path = path / filename_final
410
+
411
+ with open(final_path, "wb") as f:
412
+ f.write(data)
413
+
414
+ self.logger.info(
415
+ f"Media successfully downloaded to {final_path} ({downloaded_size} bytes)"
416
+ )
417
+
418
+ return MediaDownloadResult(
419
+ success=True,
420
+ file_data=bytes(data),
421
+ file_path=str(final_path) if final_path else None,
422
+ mime_type=response_content_type,
423
+ file_size=downloaded_size,
424
+ sha256=media_info_result.sha256,
425
+ platform=PlatformType.WHATSAPP,
426
+ tenant_id=self._tenant_id,
427
+ )
428
+
429
+ finally:
430
+ # Ensure response is closed
431
+ if response and not response.closed:
432
+ response.release()
433
+
434
+ except Exception as e:
435
+ self.logger.exception(f"Error downloading media ID {media_id}: {e}")
436
+ return MediaDownloadResult(
437
+ success=False,
438
+ error=str(e),
439
+ error_code="DOWNLOAD_FAILED",
440
+ tenant_id=self._tenant_id,
441
+ )
442
+
443
+ async def stream_media(
444
+ self, media_id: str, chunk_size: int = 8192
445
+ ) -> AsyncIterator[bytes]:
446
+ """Stream media by ID for large files."""
447
+ try:
448
+ # Get media info first
449
+ media_info_result = await self.get_media_info(media_id)
450
+ if not media_info_result.success:
451
+ raise RuntimeError(
452
+ f"Failed to get media URL for ID {media_id}: {media_info_result.error}"
453
+ )
454
+
455
+ media_url = media_info_result.url
456
+
457
+ # Use the client for streaming request
458
+ session, response = await self.client.get_request_stream(media_url)
459
+
460
+ try:
461
+ if response.status != 200:
462
+ error_text = await response.text()
463
+ raise RuntimeError(
464
+ f"Download failed for {media_id}: {response.status} - {error_text}"
465
+ )
466
+
467
+ async for chunk in response.content.iter_chunked(chunk_size):
468
+ if chunk:
469
+ yield chunk
470
+
471
+ finally:
472
+ # Ensure response is closed
473
+ if response and not response.closed:
474
+ response.release()
475
+
476
+ except Exception as e:
477
+ self.logger.exception(f"Error streaming media ID {media_id}: {e}")
478
+ raise
479
+
480
+ async def delete_media(self, media_id: str) -> MediaDeleteResult:
481
+ """
482
+ Delete media from WhatsApp servers using the media ID.
483
+
484
+ Based on existing WhatsAppServiceMedia.delete_media() method.
485
+ Implements DELETE /MEDIA_ID endpoint.
486
+ """
487
+ try:
488
+ endpoint = f"{media_id}"
489
+ params = {}
490
+
491
+ self.logger.debug(f"Attempting to delete media ID: {media_id}")
492
+
493
+ result = await self.client.delete_request(endpoint=endpoint, params=params)
494
+
495
+ if result.get("success"):
496
+ self.logger.info(f"Successfully deleted media ID: {media_id}")
497
+ return MediaDeleteResult(
498
+ success=True,
499
+ media_id=media_id,
500
+ platform=PlatformType.WHATSAPP,
501
+ tenant_id=self._tenant_id,
502
+ )
503
+ else:
504
+ error_msg = result.get("error", {}).get("message", "Unknown reason")
505
+ return MediaDeleteResult(
506
+ success=False,
507
+ media_id=media_id,
508
+ error=f"API indicated deletion failed: {error_msg}",
509
+ error_code="DELETION_FAILED",
510
+ platform=PlatformType.WHATSAPP,
511
+ tenant_id=self._tenant_id,
512
+ )
513
+
514
+ except Exception as e:
515
+ self.logger.exception(f"Error deleting media ID {media_id}: {e}")
516
+ return MediaDeleteResult(
517
+ success=False,
518
+ media_id=media_id,
519
+ error=str(e),
520
+ error_code="DELETION_FAILED",
521
+ platform=PlatformType.WHATSAPP,
522
+ tenant_id=self._tenant_id,
523
+ )
524
+
525
+ def validate_media_type(self, mime_type: str) -> bool:
526
+ """Validate if MIME type is supported by WhatsApp."""
527
+ return mime_type in self.supported_media_types
528
+
529
+ def validate_file_size(self, file_size: int, mime_type: str) -> bool:
530
+ """Validate if file size is within WhatsApp limits."""
531
+ max_size = self._get_max_size_for_mime_type(mime_type)
532
+ return file_size <= max_size
533
+
534
+ def get_media_limits(self) -> dict[str, Any]:
535
+ """Get WhatsApp-specific media limits and constraints."""
536
+ return {
537
+ "max_sizes": self.max_file_size,
538
+ "supported_types": sorted(self.supported_media_types),
539
+ "url_expiry_minutes": 5,
540
+ "media_persistence_days": 30,
541
+ "platform": "whatsapp",
542
+ "api_version": self.client.api_version,
543
+ }
544
+
545
+ def _get_max_size_for_mime_type(self, mime_type: str) -> int:
546
+ """Get maximum file size for a specific MIME type."""
547
+ if mime_type.startswith("audio/") or mime_type.startswith("video/"):
548
+ return 16 * 1024 * 1024 # 16MB
549
+ elif mime_type.startswith("image/"):
550
+ if mime_type == "image/webp":
551
+ return 500 * 1024 # 500KB for animated stickers
552
+ return 5 * 1024 * 1024 # 5MB for regular images
553
+ elif mime_type.startswith("application/") or mime_type == "text/plain":
554
+ return 100 * 1024 * 1024 # 100MB
555
+ else:
556
+ return 100 * 1024 * 1024 # Default to 100MB
557
+
558
+ def _get_extension_map(self) -> dict[str, str]:
559
+ """Get file extension mapping by MIME type."""
560
+ return {
561
+ "audio/aac": ".aac",
562
+ "audio/amr": ".amr",
563
+ "audio/mpeg": ".mp3",
564
+ "audio/mp4": ".m4a",
565
+ "audio/ogg": ".ogg",
566
+ "text/plain": ".txt",
567
+ "application/vnd.ms-excel": ".xls",
568
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
569
+ "application/msword": ".doc",
570
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
571
+ "application/vnd.ms-powerpoint": ".ppt",
572
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
573
+ "application/pdf": ".pdf",
574
+ "image/jpeg": ".jpg",
575
+ "image/png": ".png",
576
+ "image/webp": ".webp",
577
+ "video/3gpp": ".3gp",
578
+ "video/mp4": ".mp4",
579
+ }