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