wappa 0.1.8__py3-none-any.whl → 0.1.10__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 (147) hide show
  1. wappa/__init__.py +4 -5
  2. wappa/api/controllers/webhook_controller.py +5 -2
  3. wappa/api/dependencies/__init__.py +0 -5
  4. wappa/api/middleware/error_handler.py +4 -4
  5. wappa/api/middleware/owner.py +11 -5
  6. wappa/api/routes/webhooks.py +2 -2
  7. wappa/cli/__init__.py +1 -1
  8. wappa/cli/examples/init/.env.example +33 -0
  9. wappa/cli/examples/init/app/__init__.py +0 -0
  10. wappa/cli/examples/init/app/main.py +9 -0
  11. wappa/cli/examples/init/app/master_event.py +10 -0
  12. wappa/cli/examples/json_cache_example/.env.example +33 -0
  13. wappa/cli/examples/json_cache_example/app/__init__.py +1 -0
  14. wappa/cli/examples/json_cache_example/app/main.py +247 -0
  15. wappa/cli/examples/json_cache_example/app/master_event.py +455 -0
  16. wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -0
  17. wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +256 -0
  18. wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
  19. wappa/cli/examples/json_cache_example/app/scores/score_base.py +192 -0
  20. wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +256 -0
  21. wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +187 -0
  22. wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +272 -0
  23. wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +239 -0
  24. wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
  25. wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +174 -0
  26. wappa/cli/examples/json_cache_example/app/utils/message_utils.py +251 -0
  27. wappa/cli/examples/openai_transcript/.gitignore +63 -4
  28. wappa/cli/examples/openai_transcript/app/__init__.py +0 -0
  29. wappa/cli/examples/openai_transcript/app/main.py +9 -0
  30. wappa/cli/examples/openai_transcript/app/master_event.py +62 -0
  31. wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
  32. wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +89 -0
  33. wappa/cli/examples/redis_cache_example/.env.example +33 -0
  34. wappa/cli/examples/redis_cache_example/app/__init__.py +6 -0
  35. wappa/cli/examples/redis_cache_example/app/main.py +246 -0
  36. wappa/cli/examples/redis_cache_example/app/master_event.py +455 -0
  37. wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +256 -0
  38. wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
  39. wappa/cli/examples/redis_cache_example/app/scores/score_base.py +192 -0
  40. wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +256 -0
  41. wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +187 -0
  42. wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +272 -0
  43. wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +239 -0
  44. wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
  45. wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +174 -0
  46. wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +251 -0
  47. wappa/cli/examples/simple_echo_example/.env.example +33 -0
  48. wappa/cli/examples/simple_echo_example/app/__init__.py +7 -0
  49. wappa/cli/examples/simple_echo_example/app/main.py +191 -0
  50. wappa/cli/examples/simple_echo_example/app/master_event.py +230 -0
  51. wappa/cli/examples/wappa_full_example/.env.example +33 -0
  52. wappa/cli/examples/wappa_full_example/.gitignore +63 -4
  53. wappa/cli/examples/wappa_full_example/app/__init__.py +6 -0
  54. wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +5 -0
  55. wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +492 -0
  56. wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +559 -0
  57. wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +514 -0
  58. wappa/cli/examples/wappa_full_example/app/main.py +269 -0
  59. wappa/cli/examples/wappa_full_example/app/master_event.py +504 -0
  60. wappa/cli/examples/wappa_full_example/app/media/README.md +54 -0
  61. wappa/cli/examples/wappa_full_example/app/media/buttons/README.md +62 -0
  62. wappa/cli/examples/wappa_full_example/app/media/buttons/kitty.png +0 -0
  63. wappa/cli/examples/wappa_full_example/app/media/buttons/puppy.png +0 -0
  64. wappa/cli/examples/wappa_full_example/app/media/list/README.md +110 -0
  65. wappa/cli/examples/wappa_full_example/app/media/list/audio.mp3 +0 -0
  66. wappa/cli/examples/wappa_full_example/app/media/list/document.pdf +0 -0
  67. wappa/cli/examples/wappa_full_example/app/media/list/image.png +0 -0
  68. wappa/cli/examples/wappa_full_example/app/media/list/video.mp4 +0 -0
  69. wappa/cli/examples/wappa_full_example/app/models/__init__.py +5 -0
  70. wappa/cli/examples/wappa_full_example/app/models/state_models.py +434 -0
  71. wappa/cli/examples/wappa_full_example/app/models/user_models.py +303 -0
  72. wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +327 -0
  73. wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
  74. wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +502 -0
  75. wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +516 -0
  76. wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +337 -0
  77. wappa/cli/main.py +14 -5
  78. wappa/core/__init__.py +18 -23
  79. wappa/core/config/settings.py +7 -5
  80. wappa/core/events/default_handlers.py +1 -1
  81. wappa/core/factory/wappa_builder.py +38 -25
  82. wappa/core/plugins/redis_plugin.py +1 -3
  83. wappa/core/plugins/wappa_core_plugin.py +7 -6
  84. wappa/core/types.py +12 -12
  85. wappa/core/wappa_app.py +10 -8
  86. wappa/database/__init__.py +3 -4
  87. wappa/domain/enums/messenger_platform.py +1 -2
  88. wappa/domain/factories/media_factory.py +5 -20
  89. wappa/domain/factories/message_factory.py +5 -20
  90. wappa/domain/factories/messenger_factory.py +2 -4
  91. wappa/domain/interfaces/cache_interface.py +7 -7
  92. wappa/domain/interfaces/media_interface.py +2 -5
  93. wappa/domain/models/media_result.py +1 -3
  94. wappa/domain/models/platforms/platform_config.py +1 -3
  95. wappa/messaging/__init__.py +9 -12
  96. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +20 -22
  97. wappa/models/__init__.py +27 -35
  98. wappa/persistence/__init__.py +12 -15
  99. wappa/persistence/cache_factory.py +0 -1
  100. wappa/persistence/json/__init__.py +1 -1
  101. wappa/persistence/json/cache_adapters.py +37 -25
  102. wappa/persistence/json/handlers/state_handler.py +60 -52
  103. wappa/persistence/json/handlers/table_handler.py +51 -49
  104. wappa/persistence/json/handlers/user_handler.py +71 -55
  105. wappa/persistence/json/handlers/utils/file_manager.py +42 -39
  106. wappa/persistence/json/handlers/utils/key_factory.py +1 -1
  107. wappa/persistence/json/handlers/utils/serialization.py +13 -11
  108. wappa/persistence/json/json_cache_factory.py +4 -8
  109. wappa/persistence/json/storage_manager.py +66 -79
  110. wappa/persistence/memory/__init__.py +1 -1
  111. wappa/persistence/memory/cache_adapters.py +37 -25
  112. wappa/persistence/memory/handlers/state_handler.py +62 -52
  113. wappa/persistence/memory/handlers/table_handler.py +59 -53
  114. wappa/persistence/memory/handlers/user_handler.py +75 -55
  115. wappa/persistence/memory/handlers/utils/key_factory.py +1 -1
  116. wappa/persistence/memory/handlers/utils/memory_store.py +75 -71
  117. wappa/persistence/memory/handlers/utils/ttl_manager.py +59 -67
  118. wappa/persistence/memory/memory_cache_factory.py +3 -7
  119. wappa/persistence/memory/storage_manager.py +52 -62
  120. wappa/persistence/redis/cache_adapters.py +27 -21
  121. wappa/persistence/redis/ops.py +11 -11
  122. wappa/persistence/redis/redis_client.py +4 -6
  123. wappa/persistence/redis/redis_manager.py +12 -4
  124. wappa/processors/factory.py +5 -5
  125. wappa/schemas/factory.py +2 -5
  126. wappa/schemas/whatsapp/message_types/errors.py +3 -12
  127. wappa/schemas/whatsapp/validators.py +3 -3
  128. wappa/webhooks/__init__.py +17 -18
  129. wappa/webhooks/factory.py +3 -5
  130. wappa/webhooks/whatsapp/__init__.py +10 -13
  131. wappa/webhooks/whatsapp/message_types/audio.py +0 -4
  132. wappa/webhooks/whatsapp/message_types/document.py +1 -9
  133. wappa/webhooks/whatsapp/message_types/errors.py +3 -12
  134. wappa/webhooks/whatsapp/message_types/location.py +1 -21
  135. wappa/webhooks/whatsapp/message_types/sticker.py +1 -5
  136. wappa/webhooks/whatsapp/message_types/text.py +0 -6
  137. wappa/webhooks/whatsapp/message_types/video.py +1 -20
  138. wappa/webhooks/whatsapp/status_models.py +2 -2
  139. wappa/webhooks/whatsapp/validators.py +3 -3
  140. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/METADATA +362 -8
  141. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/RECORD +144 -80
  142. wappa/cli/examples/init/pyproject.toml +0 -7
  143. wappa/cli/examples/simple_echo_example/.python-version +0 -1
  144. wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
  145. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/WHEEL +0 -0
  146. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/entry_points.txt +0 -0
  147. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,516 @@
1
+ """
2
+ Media handling utilities for the Wappa Full Example application.
3
+
4
+ This module provides functions for downloading and uploading media files,
5
+ handling different media types, and managing local media storage.
6
+ """
7
+
8
+ import os
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ import aiohttp
13
+
14
+ from wappa.webhooks import IncomingMessageWebhook
15
+
16
+
17
+ class MediaHandler:
18
+ """Utility class for handling media operations."""
19
+
20
+ def __init__(self, temp_dir: str | None = None):
21
+ """
22
+ Initialize MediaHandler.
23
+
24
+ Args:
25
+ temp_dir: Optional temporary directory path for downloads
26
+ """
27
+ self.temp_dir = temp_dir or tempfile.gettempdir()
28
+ self.session: aiohttp.ClientSession | None = None
29
+
30
+ async def __aenter__(self):
31
+ """Async context manager entry."""
32
+ self.session = aiohttp.ClientSession()
33
+ return self
34
+
35
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
36
+ """Async context manager exit."""
37
+ if self.session:
38
+ await self.session.close()
39
+
40
+ async def get_media_info_from_webhook(
41
+ self, webhook: IncomingMessageWebhook
42
+ ) -> dict[str, str] | None:
43
+ """
44
+ Extract media information from webhook.
45
+
46
+ Args:
47
+ webhook: IncomingMessageWebhook containing media
48
+
49
+ Returns:
50
+ Dictionary with media info or None if no media found
51
+ """
52
+ message = webhook.message
53
+ message_type = webhook.get_message_type_name().lower()
54
+
55
+ if message_type not in [
56
+ "image",
57
+ "video",
58
+ "audio",
59
+ "voice",
60
+ "document",
61
+ "sticker",
62
+ ]:
63
+ return None
64
+
65
+ media_info = {"type": message_type, "message_id": message.message_id}
66
+
67
+ # Try to extract media ID (different field names possible)
68
+ media_id = None
69
+ for field_name in ["media_id", "id", f"{message_type}_id"]:
70
+ if hasattr(message, field_name):
71
+ media_id = getattr(message, field_name)
72
+ if media_id:
73
+ break
74
+
75
+ if not media_id:
76
+ return None
77
+
78
+ media_info["media_id"] = media_id
79
+
80
+ # Extract additional metadata
81
+ if hasattr(message, "mime_type"):
82
+ media_info["mime_type"] = message.mime_type
83
+
84
+ if hasattr(message, "file_size"):
85
+ media_info["file_size"] = message.file_size
86
+
87
+ if hasattr(message, "filename"):
88
+ media_info["filename"] = message.filename
89
+
90
+ if hasattr(message, "caption"):
91
+ media_info["caption"] = message.caption
92
+
93
+ # For images and videos, get dimensions
94
+ if message_type in ["image", "video"]:
95
+ if hasattr(message, "width"):
96
+ media_info["width"] = message.width
97
+ if hasattr(message, "height"):
98
+ media_info["height"] = message.height
99
+
100
+ # For audio and video, get duration
101
+ if message_type in ["audio", "video", "voice"]:
102
+ if hasattr(message, "duration"):
103
+ media_info["duration"] = message.duration
104
+
105
+ return media_info
106
+
107
+ async def download_media_by_id(
108
+ self, media_id: str, messenger, media_type: str = None
109
+ ) -> tuple[str, dict[str, str]] | None:
110
+ """
111
+ Download media using media_id through WhatsApp API.
112
+
113
+ Note: This is a placeholder implementation. The actual implementation
114
+ would need to integrate with the WhatsApp Business API to download media.
115
+
116
+ Args:
117
+ media_id: Media ID from WhatsApp
118
+ messenger: IMessenger instance for API calls
119
+ media_type: Optional media type hint
120
+
121
+ Returns:
122
+ Tuple of (file_path, metadata) or None if failed
123
+ """
124
+ try:
125
+ # This is a placeholder implementation
126
+ # In a real implementation, you would:
127
+ # 1. Use messenger or direct API calls to get media URL
128
+ # 2. Download the media file
129
+ # 3. Save to temporary location
130
+ # 4. Return file path and metadata
131
+
132
+ # For now, return None to indicate media download not implemented
133
+ return None
134
+
135
+ except Exception as e:
136
+ print(f"Error downloading media {media_id}: {e}")
137
+ return None
138
+
139
+ async def upload_local_media(
140
+ self, file_path: str, media_type: str = None
141
+ ) -> str | None:
142
+ """
143
+ Upload local media file to get media_id for sending.
144
+
145
+ Note: This is a placeholder implementation. The actual implementation
146
+ would need to integrate with the WhatsApp Business API media upload endpoint.
147
+
148
+ Args:
149
+ file_path: Path to local media file
150
+ media_type: Type of media (image, video, audio, document)
151
+
152
+ Returns:
153
+ Media ID if successful, None if failed
154
+ """
155
+ try:
156
+ # This is a placeholder implementation
157
+ # In a real implementation, you would:
158
+ # 1. Upload the file to WhatsApp Business API
159
+ # 2. Get the media_id from the response
160
+ # 3. Return the media_id
161
+
162
+ # For now, return the file path as a placeholder
163
+ if os.path.exists(file_path):
164
+ return f"local_{os.path.basename(file_path)}"
165
+
166
+ return None
167
+
168
+ except Exception as e:
169
+ print(f"Error uploading media {file_path}: {e}")
170
+ return None
171
+
172
+ def get_local_media_path(self, filename: str, media_subdir: str = None) -> str:
173
+ """
174
+ Get path to local media file.
175
+
176
+ Args:
177
+ filename: Name of the media file
178
+ media_subdir: Optional subdirectory within media folder
179
+
180
+ Returns:
181
+ Full path to media file
182
+ """
183
+ # Construct path relative to app directory
184
+ base_dir = Path(__file__).parent.parent # Go up to app directory
185
+ media_dir = base_dir / "media"
186
+
187
+ if media_subdir:
188
+ media_dir = media_dir / media_subdir
189
+
190
+ return str(media_dir / filename)
191
+
192
+ def media_file_exists(self, filename: str, media_subdir: str = None) -> bool:
193
+ """
194
+ Check if local media file exists.
195
+
196
+ Args:
197
+ filename: Name of the media file
198
+ media_subdir: Optional subdirectory within media folder
199
+
200
+ Returns:
201
+ True if file exists, False otherwise
202
+ """
203
+ file_path = self.get_local_media_path(filename, media_subdir)
204
+ return os.path.exists(file_path)
205
+
206
+ def get_media_type_from_extension(self, filename: str) -> str:
207
+ """
208
+ Determine media type from file extension.
209
+
210
+ Args:
211
+ filename: Name of the file
212
+
213
+ Returns:
214
+ Media type string
215
+ """
216
+ extension = Path(filename).suffix.lower()
217
+
218
+ # Image extensions
219
+ if extension in [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"]:
220
+ return "image"
221
+
222
+ # Video extensions
223
+ elif extension in [".mp4", ".avi", ".mov", ".wmv", ".flv", ".webm"]:
224
+ return "video"
225
+
226
+ # Audio extensions
227
+ elif extension in [".mp3", ".wav", ".aac", ".ogg", ".m4a", ".flac"]:
228
+ return "audio"
229
+
230
+ # Document extensions
231
+ elif extension in [
232
+ ".pdf",
233
+ ".doc",
234
+ ".docx",
235
+ ".xls",
236
+ ".xlsx",
237
+ ".ppt",
238
+ ".pptx",
239
+ ".txt",
240
+ ]:
241
+ return "document"
242
+
243
+ # Default
244
+ else:
245
+ return "document"
246
+
247
+ async def send_media_by_file(
248
+ self,
249
+ messenger,
250
+ recipient: str,
251
+ file_path: str,
252
+ caption: str = None,
253
+ reply_to_message_id: str = None,
254
+ ) -> dict[str, any]:
255
+ """
256
+ Send media file using appropriate messenger method.
257
+
258
+ Args:
259
+ messenger: IMessenger instance
260
+ recipient: Recipient phone number
261
+ file_path: Path to media file
262
+ caption: Optional caption
263
+ reply_to_message_id: Optional message to reply to
264
+
265
+ Returns:
266
+ Result dictionary with success status and details
267
+ """
268
+ try:
269
+ if not os.path.exists(file_path):
270
+ return {
271
+ "success": False,
272
+ "error": f"File not found: {file_path}",
273
+ "method": "file_not_found",
274
+ }
275
+
276
+ media_type = self.get_media_type_from_extension(file_path)
277
+
278
+ # Send using appropriate method based on media type
279
+ if media_type == "image":
280
+ result = await messenger.send_image(
281
+ image_source=file_path,
282
+ recipient=recipient,
283
+ caption=caption,
284
+ reply_to_message_id=reply_to_message_id,
285
+ )
286
+
287
+ elif media_type == "video":
288
+ result = await messenger.send_video(
289
+ video_source=file_path,
290
+ recipient=recipient,
291
+ caption=caption,
292
+ reply_to_message_id=reply_to_message_id,
293
+ )
294
+
295
+ elif media_type == "audio":
296
+ result = await messenger.send_audio(
297
+ audio_source=file_path,
298
+ recipient=recipient,
299
+ reply_to_message_id=reply_to_message_id,
300
+ )
301
+
302
+ elif media_type == "document":
303
+ filename = os.path.basename(file_path)
304
+ result = await messenger.send_document(
305
+ document_source=file_path,
306
+ recipient=recipient,
307
+ filename=filename,
308
+ caption=caption,
309
+ reply_to_message_id=reply_to_message_id,
310
+ )
311
+
312
+ else:
313
+ return {
314
+ "success": False,
315
+ "error": f"Unsupported media type: {media_type}",
316
+ "method": "unsupported_type",
317
+ }
318
+
319
+ return {
320
+ "success": result.success,
321
+ "message_id": result.message_id
322
+ if hasattr(result, "message_id")
323
+ else None,
324
+ "error": result.error if hasattr(result, "error") else None,
325
+ "method": f"send_{media_type}",
326
+ "media_type": media_type,
327
+ "file_path": file_path,
328
+ }
329
+
330
+ except Exception as e:
331
+ return {
332
+ "success": False,
333
+ "error": f"Error sending media: {str(e)}",
334
+ "method": "exception",
335
+ "file_path": file_path,
336
+ }
337
+
338
+ async def send_media_by_id(
339
+ self,
340
+ messenger,
341
+ recipient: str,
342
+ media_id: str,
343
+ media_type: str,
344
+ caption: str = None,
345
+ reply_to_message_id: str = None,
346
+ ) -> dict[str, any]:
347
+ """
348
+ Send media using media_id (relay existing media).
349
+
350
+ Args:
351
+ messenger: IMessenger instance
352
+ recipient: Recipient phone number
353
+ media_id: Media ID to send
354
+ media_type: Type of media
355
+ caption: Optional caption
356
+ reply_to_message_id: Optional message to reply to
357
+
358
+ Returns:
359
+ Result dictionary with success status and details
360
+ """
361
+ try:
362
+ # For relaying media using media_id, we need to use the media_id as source
363
+ # This assumes the messenger can handle media_id as source parameter
364
+
365
+ if media_type == "image":
366
+ result = await messenger.send_image(
367
+ image_source=media_id, # Using media_id as source
368
+ recipient=recipient,
369
+ caption=caption,
370
+ reply_to_message_id=reply_to_message_id,
371
+ )
372
+
373
+ elif media_type == "video":
374
+ result = await messenger.send_video(
375
+ video_source=media_id,
376
+ recipient=recipient,
377
+ caption=caption,
378
+ reply_to_message_id=reply_to_message_id,
379
+ )
380
+
381
+ elif media_type in ["audio", "voice"]:
382
+ result = await messenger.send_audio(
383
+ audio_source=media_id,
384
+ recipient=recipient,
385
+ reply_to_message_id=reply_to_message_id,
386
+ )
387
+
388
+ elif media_type == "document":
389
+ result = await messenger.send_document(
390
+ document_source=media_id,
391
+ recipient=recipient,
392
+ caption=caption,
393
+ reply_to_message_id=reply_to_message_id,
394
+ )
395
+
396
+ elif media_type == "sticker":
397
+ result = await messenger.send_sticker(
398
+ sticker_source=media_id,
399
+ recipient=recipient,
400
+ reply_to_message_id=reply_to_message_id,
401
+ )
402
+
403
+ else:
404
+ return {
405
+ "success": False,
406
+ "error": f"Unsupported media type for relay: {media_type}",
407
+ "method": "unsupported_relay_type",
408
+ }
409
+
410
+ return {
411
+ "success": result.success,
412
+ "message_id": result.message_id
413
+ if hasattr(result, "message_id")
414
+ else None,
415
+ "error": result.error if hasattr(result, "error") else None,
416
+ "method": f"relay_{media_type}",
417
+ "media_type": media_type,
418
+ "media_id": media_id,
419
+ }
420
+
421
+ except Exception as e:
422
+ return {
423
+ "success": False,
424
+ "error": f"Error relaying media: {str(e)}",
425
+ "method": "relay_exception",
426
+ "media_id": media_id,
427
+ }
428
+
429
+
430
+ # Convenience functions for direct use
431
+ async def extract_media_info(webhook: IncomingMessageWebhook) -> dict[str, str] | None:
432
+ """
433
+ Extract media information from webhook (convenience function).
434
+
435
+ Args:
436
+ webhook: IncomingMessageWebhook to process
437
+
438
+ Returns:
439
+ Media info dictionary or None
440
+ """
441
+ handler = MediaHandler()
442
+ return await handler.get_media_info_from_webhook(webhook)
443
+
444
+
445
+ async def send_local_media_file(
446
+ messenger,
447
+ recipient: str,
448
+ filename: str,
449
+ media_subdir: str = None,
450
+ caption: str = None,
451
+ reply_to_message_id: str = None,
452
+ ) -> dict[str, any]:
453
+ """
454
+ Send local media file (convenience function).
455
+
456
+ Args:
457
+ messenger: IMessenger instance
458
+ recipient: Recipient phone number
459
+ filename: Name of media file in media directory
460
+ media_subdir: Optional subdirectory
461
+ caption: Optional caption
462
+ reply_to_message_id: Optional message to reply to
463
+
464
+ Returns:
465
+ Result dictionary
466
+ """
467
+ handler = MediaHandler()
468
+ file_path = handler.get_local_media_path(filename, media_subdir)
469
+
470
+ return await handler.send_media_by_file(
471
+ messenger=messenger,
472
+ recipient=recipient,
473
+ file_path=file_path,
474
+ caption=caption,
475
+ reply_to_message_id=reply_to_message_id,
476
+ )
477
+
478
+
479
+ async def relay_webhook_media(
480
+ messenger,
481
+ webhook: IncomingMessageWebhook,
482
+ recipient: str,
483
+ reply_to_message_id: str = None,
484
+ ) -> dict[str, any]:
485
+ """
486
+ Relay media from webhook to recipient (convenience function).
487
+
488
+ Args:
489
+ messenger: IMessenger instance
490
+ webhook: Original webhook with media
491
+ recipient: Recipient phone number
492
+ reply_to_message_id: Optional message to reply to
493
+
494
+ Returns:
495
+ Result dictionary
496
+ """
497
+ handler = MediaHandler()
498
+
499
+ # Extract media info from webhook
500
+ media_info = await handler.get_media_info_from_webhook(webhook)
501
+ if not media_info:
502
+ return {
503
+ "success": False,
504
+ "error": "No media found in webhook",
505
+ "method": "no_media",
506
+ }
507
+
508
+ # Relay using media_id
509
+ return await handler.send_media_by_id(
510
+ messenger=messenger,
511
+ recipient=recipient,
512
+ media_id=media_info["media_id"],
513
+ media_type=media_info["type"],
514
+ caption=media_info.get("caption"),
515
+ reply_to_message_id=reply_to_message_id,
516
+ )