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