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,559 @@
1
+ """
2
+ Message handlers for different message types in the Wappa Full Example application.
3
+
4
+ This module provides handlers for processing and echoing different types of messages
5
+ including text, media, location, contact, and interactive messages.
6
+ """
7
+
8
+ import time
9
+
10
+ from wappa.webhooks import IncomingMessageWebhook
11
+
12
+ from ..models.user_models import UserProfile
13
+ from ..utils.cache_utils import CacheHelper
14
+ from ..utils.media_handler import MediaHandler, relay_webhook_media
15
+ from ..utils.metadata_extractor import MetadataExtractor
16
+
17
+
18
+ class MessageHandlers:
19
+ """Collection of message handlers for different message types."""
20
+
21
+ def __init__(self, messenger, cache_factory, logger):
22
+ """
23
+ Initialize message handlers.
24
+
25
+ Args:
26
+ messenger: IMessenger instance for sending messages
27
+ cache_factory: Cache factory for data persistence
28
+ logger: Logger instance
29
+ """
30
+ self.messenger = messenger
31
+ self.cache_helper = CacheHelper(cache_factory)
32
+ self.media_handler = MediaHandler()
33
+ self.logger = logger
34
+
35
+ async def handle_text_message(
36
+ self, webhook: IncomingMessageWebhook, user_profile: UserProfile
37
+ ) -> dict[str, any]:
38
+ """
39
+ Handle text message with echo functionality.
40
+
41
+ Args:
42
+ webhook: IncomingMessageWebhook with text message
43
+ user_profile: User profile for tracking
44
+
45
+ Returns:
46
+ Result dictionary with operation status
47
+ """
48
+ try:
49
+ start_time = time.time()
50
+
51
+ # Extract message content
52
+ text_content = webhook.get_message_text()
53
+ user_id = webhook.user.user_id
54
+ message_id = webhook.message.message_id
55
+
56
+ self.logger.info(
57
+ f"📝 Processing text message from {user_id}: '{text_content[:50]}'"
58
+ )
59
+
60
+ # Extract and format metadata
61
+ metadata = MetadataExtractor.extract_metadata(webhook, start_time)
62
+ metadata_text = MetadataExtractor.format_metadata_for_user(metadata)
63
+
64
+ # Send metadata response
65
+ metadata_result = await self.messenger.send_text(
66
+ recipient=user_id, text=metadata_text, reply_to_message_id=message_id
67
+ )
68
+
69
+ if not metadata_result.success:
70
+ self.logger.error(f"Failed to send metadata: {metadata_result.error}")
71
+ return {"success": False, "error": "Failed to send metadata"}
72
+
73
+ # Send echo response
74
+ echo_text = f"Echo - {text_content}"
75
+ echo_result = await self.messenger.send_text(
76
+ recipient=user_id, text=echo_text
77
+ )
78
+
79
+ if not echo_result.success:
80
+ self.logger.error(f"Failed to send echo: {echo_result.error}")
81
+ return {"success": False, "error": "Failed to send echo"}
82
+
83
+ # Update user activity
84
+ await self.cache_helper.update_user_activity(user_id, "text")
85
+
86
+ # Store message in history
87
+ await self.cache_helper.store_message_history(
88
+ user_id,
89
+ {
90
+ "message_id": message_id,
91
+ "type": "text",
92
+ "content": text_content,
93
+ "timestamp": webhook.timestamp.isoformat(),
94
+ "echo_sent": True,
95
+ },
96
+ )
97
+
98
+ processing_time = int((time.time() - start_time) * 1000)
99
+ self.logger.info(f"✅ Text message processed in {processing_time}ms")
100
+
101
+ return {
102
+ "success": True,
103
+ "message_type": "text",
104
+ "metadata_sent": True,
105
+ "echo_sent": True,
106
+ "processing_time_ms": processing_time,
107
+ }
108
+
109
+ except Exception as e:
110
+ self.logger.error(f"❌ Error handling text message: {e}", exc_info=True)
111
+ return {"success": False, "error": str(e)}
112
+
113
+ async def handle_media_message(
114
+ self, webhook: IncomingMessageWebhook, user_profile: UserProfile
115
+ ) -> dict[str, any]:
116
+ """
117
+ Handle media message with relay functionality.
118
+
119
+ Args:
120
+ webhook: IncomingMessageWebhook with media message
121
+ user_profile: User profile for tracking
122
+
123
+ Returns:
124
+ Result dictionary with operation status
125
+ """
126
+ try:
127
+ start_time = time.time()
128
+
129
+ user_id = webhook.user.user_id
130
+ message_id = webhook.message.message_id
131
+ message_type = webhook.get_message_type_name()
132
+
133
+ self.logger.info(f"🎬 Processing {message_type} message from {user_id}")
134
+
135
+ # Extract media information
136
+ media_info = await self.media_handler.get_media_info_from_webhook(webhook)
137
+ if not media_info:
138
+ return {"success": False, "error": "No media found in webhook"}
139
+
140
+ # Extract and format metadata
141
+ metadata = MetadataExtractor.extract_metadata(webhook, start_time)
142
+ metadata_text = MetadataExtractor.format_metadata_for_user(metadata)
143
+
144
+ # Send metadata response
145
+ metadata_result = await self.messenger.send_text(
146
+ recipient=user_id, text=metadata_text, reply_to_message_id=message_id
147
+ )
148
+
149
+ if not metadata_result.success:
150
+ self.logger.error(f"Failed to send metadata: {metadata_result.error}")
151
+ return {"success": False, "error": "Failed to send metadata"}
152
+
153
+ # Relay the same media using media_id
154
+ relay_result = await relay_webhook_media(
155
+ messenger=self.messenger, webhook=webhook, recipient=user_id
156
+ )
157
+
158
+ if not relay_result["success"]:
159
+ self.logger.error(f"Failed to relay media: {relay_result.get('error')}")
160
+
161
+ # Send fallback text response if media relay fails
162
+ fallback_text = f"📎 Media echo - {message_type} (relay failed, media_id: {media_info.get('media_id', 'unknown')[:20]}...)"
163
+ await self.messenger.send_text(recipient=user_id, text=fallback_text)
164
+
165
+ # Update user activity
166
+ await self.cache_helper.update_user_activity(user_id, "media")
167
+
168
+ # Store message in history
169
+ await self.cache_helper.store_message_history(
170
+ user_id,
171
+ {
172
+ "message_id": message_id,
173
+ "type": message_type,
174
+ "media_id": media_info.get("media_id"),
175
+ "media_type": media_info.get("type"),
176
+ "timestamp": webhook.timestamp.isoformat(),
177
+ "relay_success": relay_result["success"],
178
+ },
179
+ )
180
+
181
+ processing_time = int((time.time() - start_time) * 1000)
182
+ self.logger.info(
183
+ f"✅ {message_type} message processed in {processing_time}ms"
184
+ )
185
+
186
+ return {
187
+ "success": True,
188
+ "message_type": message_type,
189
+ "metadata_sent": True,
190
+ "media_relayed": relay_result["success"],
191
+ "media_info": media_info,
192
+ "processing_time_ms": processing_time,
193
+ }
194
+
195
+ except Exception as e:
196
+ self.logger.error(f"❌ Error handling media message: {e}", exc_info=True)
197
+ return {"success": False, "error": str(e)}
198
+
199
+ async def handle_location_message(
200
+ self, webhook: IncomingMessageWebhook, user_profile: UserProfile
201
+ ) -> dict[str, any]:
202
+ """
203
+ Handle location message with echo functionality.
204
+
205
+ Args:
206
+ webhook: IncomingMessageWebhook with location message
207
+ user_profile: User profile for tracking
208
+
209
+ Returns:
210
+ Result dictionary with operation status
211
+ """
212
+ try:
213
+ start_time = time.time()
214
+
215
+ user_id = webhook.user.user_id
216
+ message_id = webhook.message.message_id
217
+
218
+ self.logger.info(f"📍 Processing location message from {user_id}")
219
+
220
+ # Extract location data
221
+ latitude = getattr(webhook.message, "latitude", None)
222
+ longitude = getattr(webhook.message, "longitude", None)
223
+ location_name = getattr(webhook.message, "name", None)
224
+ location_address = getattr(webhook.message, "address", None)
225
+
226
+ if latitude is None or longitude is None:
227
+ return {"success": False, "error": "Invalid location data"}
228
+
229
+ # Extract and format metadata
230
+ metadata = MetadataExtractor.extract_metadata(webhook, start_time)
231
+ metadata_text = MetadataExtractor.format_metadata_for_user(metadata)
232
+
233
+ # Send metadata response
234
+ metadata_result = await self.messenger.send_text(
235
+ recipient=user_id, text=metadata_text, reply_to_message_id=message_id
236
+ )
237
+
238
+ if not metadata_result.success:
239
+ self.logger.error(f"Failed to send metadata: {metadata_result.error}")
240
+ return {"success": False, "error": "Failed to send metadata"}
241
+
242
+ # Echo the same location
243
+ location_result = await self.messenger.send_location(
244
+ latitude=float(latitude),
245
+ longitude=float(longitude),
246
+ recipient=user_id,
247
+ name=location_name,
248
+ address=location_address,
249
+ )
250
+
251
+ if not location_result.success:
252
+ self.logger.error(f"Failed to send location: {location_result.error}")
253
+
254
+ # Send fallback text response
255
+ fallback_text = f"📍 Location echo - {latitude}, {longitude}"
256
+ if location_name:
257
+ fallback_text += f" ({location_name})"
258
+ await self.messenger.send_text(recipient=user_id, text=fallback_text)
259
+
260
+ # Update user activity
261
+ await self.cache_helper.update_user_activity(user_id, "location")
262
+
263
+ # Store message in history
264
+ await self.cache_helper.store_message_history(
265
+ user_id,
266
+ {
267
+ "message_id": message_id,
268
+ "type": "location",
269
+ "latitude": latitude,
270
+ "longitude": longitude,
271
+ "name": location_name,
272
+ "address": location_address,
273
+ "timestamp": webhook.timestamp.isoformat(),
274
+ "echo_sent": location_result.success,
275
+ },
276
+ )
277
+
278
+ processing_time = int((time.time() - start_time) * 1000)
279
+ self.logger.info(f"✅ Location message processed in {processing_time}ms")
280
+
281
+ return {
282
+ "success": True,
283
+ "message_type": "location",
284
+ "metadata_sent": True,
285
+ "location_echoed": location_result.success,
286
+ "coordinates": {"latitude": latitude, "longitude": longitude},
287
+ "processing_time_ms": processing_time,
288
+ }
289
+
290
+ except Exception as e:
291
+ self.logger.error(f"❌ Error handling location message: {e}", exc_info=True)
292
+ return {"success": False, "error": str(e)}
293
+
294
+ async def handle_contact_message(
295
+ self, webhook: IncomingMessageWebhook, user_profile: UserProfile
296
+ ) -> dict[str, any]:
297
+ """
298
+ Handle contact message with echo functionality.
299
+
300
+ Args:
301
+ webhook: IncomingMessageWebhook with contact message
302
+ user_profile: User profile for tracking
303
+
304
+ Returns:
305
+ Result dictionary with operation status
306
+ """
307
+ try:
308
+ start_time = time.time()
309
+
310
+ user_id = webhook.user.user_id
311
+ message_id = webhook.message.message_id
312
+
313
+ self.logger.info(f"👥 Processing contact message from {user_id}")
314
+
315
+ # Extract contact data
316
+ contacts = getattr(webhook.message, "contacts", [])
317
+ if not isinstance(contacts, list):
318
+ contacts = [contacts] if contacts else []
319
+
320
+ if not contacts:
321
+ return {"success": False, "error": "No contact data found"}
322
+
323
+ # Extract and format metadata
324
+ metadata = MetadataExtractor.extract_metadata(webhook, start_time)
325
+ metadata_text = MetadataExtractor.format_metadata_for_user(metadata)
326
+
327
+ # Send metadata response
328
+ metadata_result = await self.messenger.send_text(
329
+ recipient=user_id, text=metadata_text, reply_to_message_id=message_id
330
+ )
331
+
332
+ if not metadata_result.success:
333
+ self.logger.error(f"Failed to send metadata: {metadata_result.error}")
334
+ return {"success": False, "error": "Failed to send metadata"}
335
+
336
+ # Echo the contacts (send the first contact)
337
+ contact_to_send = contacts[0] if contacts else None
338
+ contact_sent = False
339
+
340
+ if contact_to_send:
341
+ # Convert contact to the format expected by messenger
342
+ contact_dict = self._convert_contact_to_dict(contact_to_send)
343
+
344
+ contact_result = await self.messenger.send_contact(
345
+ contact=contact_dict, recipient=user_id
346
+ )
347
+
348
+ contact_sent = contact_result.success
349
+
350
+ if not contact_sent:
351
+ self.logger.error(f"Failed to send contact: {contact_result.error}")
352
+
353
+ # Send fallback text response
354
+ contact_name = self._extract_contact_name(contact_to_send)
355
+ fallback_text = f"👤 Contact echo - {contact_name} (relay failed)"
356
+ await self.messenger.send_text(
357
+ recipient=user_id, text=fallback_text
358
+ )
359
+
360
+ # Update user activity
361
+ await self.cache_helper.update_user_activity(user_id, "contact")
362
+
363
+ # Store message in history
364
+ await self.cache_helper.store_message_history(
365
+ user_id,
366
+ {
367
+ "message_id": message_id,
368
+ "type": "contact",
369
+ "contacts_count": len(contacts),
370
+ "timestamp": webhook.timestamp.isoformat(),
371
+ "echo_sent": contact_sent,
372
+ },
373
+ )
374
+
375
+ processing_time = int((time.time() - start_time) * 1000)
376
+ self.logger.info(f"✅ Contact message processed in {processing_time}ms")
377
+
378
+ return {
379
+ "success": True,
380
+ "message_type": "contact",
381
+ "metadata_sent": True,
382
+ "contact_echoed": contact_sent,
383
+ "contacts_count": len(contacts),
384
+ "processing_time_ms": processing_time,
385
+ }
386
+
387
+ except Exception as e:
388
+ self.logger.error(f"❌ Error handling contact message: {e}", exc_info=True)
389
+ return {"success": False, "error": str(e)}
390
+
391
+ async def handle_interactive_message(
392
+ self, webhook: IncomingMessageWebhook, user_profile: UserProfile
393
+ ) -> dict[str, any]:
394
+ """
395
+ Handle interactive message (button/list selections).
396
+
397
+ Args:
398
+ webhook: IncomingMessageWebhook with interactive message
399
+ user_profile: User profile for tracking
400
+
401
+ Returns:
402
+ Result dictionary with operation status
403
+ """
404
+ try:
405
+ start_time = time.time()
406
+
407
+ user_id = webhook.user.user_id
408
+ message_id = webhook.message.message_id
409
+ selection_id = webhook.get_interactive_selection()
410
+
411
+ self.logger.info(
412
+ f"🔘 Processing interactive message from {user_id}: {selection_id}"
413
+ )
414
+
415
+ # Extract and format metadata
416
+ metadata = MetadataExtractor.extract_metadata(webhook, start_time)
417
+ metadata_text = MetadataExtractor.format_metadata_for_user(metadata)
418
+
419
+ # Send metadata response
420
+ metadata_result = await self.messenger.send_text(
421
+ recipient=user_id, text=metadata_text, reply_to_message_id=message_id
422
+ )
423
+
424
+ if not metadata_result.success:
425
+ self.logger.error(f"Failed to send metadata: {metadata_result.error}")
426
+ return {"success": False, "error": "Failed to send metadata"}
427
+
428
+ # Send acknowledgment of selection
429
+ ack_text = f"✅ Interactive selection acknowledged: `{selection_id}`"
430
+ await self.messenger.send_text(recipient=user_id, text=ack_text)
431
+
432
+ # Update user activity
433
+ await self.cache_helper.update_user_activity(user_id, "interactive")
434
+
435
+ # Store message in history
436
+ await self.cache_helper.store_message_history(
437
+ user_id,
438
+ {
439
+ "message_id": message_id,
440
+ "type": "interactive",
441
+ "selection_id": selection_id,
442
+ "timestamp": webhook.timestamp.isoformat(),
443
+ "processed": True,
444
+ },
445
+ )
446
+
447
+ processing_time = int((time.time() - start_time) * 1000)
448
+ self.logger.info(f"✅ Interactive message processed in {processing_time}ms")
449
+
450
+ return {
451
+ "success": True,
452
+ "message_type": "interactive",
453
+ "metadata_sent": True,
454
+ "selection_acknowledged": True,
455
+ "selection_id": selection_id,
456
+ "processing_time_ms": processing_time,
457
+ }
458
+
459
+ except Exception as e:
460
+ self.logger.error(
461
+ f"❌ Error handling interactive message: {e}", exc_info=True
462
+ )
463
+ return {"success": False, "error": str(e)}
464
+
465
+ def _convert_contact_to_dict(self, contact_obj) -> dict[str, any]:
466
+ """Convert contact object to dictionary format for messenger."""
467
+ contact_dict = {}
468
+
469
+ # Extract name information
470
+ if hasattr(contact_obj, "name") and contact_obj.name:
471
+ name_obj = contact_obj.name
472
+ contact_dict["name"] = {}
473
+
474
+ if hasattr(name_obj, "formatted_name"):
475
+ contact_dict["name"]["formatted_name"] = name_obj.formatted_name
476
+ if hasattr(name_obj, "first_name"):
477
+ contact_dict["name"]["first_name"] = name_obj.first_name
478
+ if hasattr(name_obj, "last_name"):
479
+ contact_dict["name"]["last_name"] = name_obj.last_name
480
+
481
+ # Extract phone numbers
482
+ if hasattr(contact_obj, "phones") and contact_obj.phones:
483
+ phones = contact_obj.phones
484
+ if not isinstance(phones, list):
485
+ phones = [phones]
486
+ contact_dict["phones"] = []
487
+ for phone in phones:
488
+ if hasattr(phone, "phone"):
489
+ phone_dict = {"phone": phone.phone}
490
+ if hasattr(phone, "type"):
491
+ phone_dict["type"] = phone.type
492
+ contact_dict["phones"].append(phone_dict)
493
+
494
+ # Extract emails
495
+ if hasattr(contact_obj, "emails") and contact_obj.emails:
496
+ emails = contact_obj.emails
497
+ if not isinstance(emails, list):
498
+ emails = [emails]
499
+ contact_dict["emails"] = []
500
+ for email in emails:
501
+ if hasattr(email, "email"):
502
+ email_dict = {"email": email.email}
503
+ if hasattr(email, "type"):
504
+ email_dict["type"] = email.type
505
+ contact_dict["emails"].append(email_dict)
506
+
507
+ return contact_dict
508
+
509
+ def _extract_contact_name(self, contact_obj) -> str:
510
+ """Extract contact name for display."""
511
+ if hasattr(contact_obj, "name") and contact_obj.name:
512
+ name_obj = contact_obj.name
513
+ if hasattr(name_obj, "formatted_name"):
514
+ return name_obj.formatted_name
515
+ elif hasattr(name_obj, "first_name"):
516
+ first = name_obj.first_name or ""
517
+ last = getattr(name_obj, "last_name", "") or ""
518
+ return f"{first} {last}".strip()
519
+
520
+ return "Unknown Contact"
521
+
522
+
523
+ # Convenience functions for direct use
524
+ async def handle_message_by_type(
525
+ webhook: IncomingMessageWebhook,
526
+ user_profile: UserProfile,
527
+ messenger,
528
+ cache_factory,
529
+ logger,
530
+ ) -> dict[str, any]:
531
+ """
532
+ Handle message based on its type (convenience function).
533
+
534
+ Args:
535
+ webhook: IncomingMessageWebhook to process
536
+ user_profile: User profile for tracking
537
+ messenger: IMessenger instance
538
+ cache_factory: Cache factory
539
+ logger: Logger instance
540
+
541
+ Returns:
542
+ Result dictionary
543
+ """
544
+ handlers = MessageHandlers(messenger, cache_factory, logger)
545
+ message_type = webhook.get_message_type_name().lower()
546
+
547
+ if message_type == "text":
548
+ return await handlers.handle_text_message(webhook, user_profile)
549
+ elif message_type in ["image", "video", "audio", "voice", "document", "sticker"]:
550
+ return await handlers.handle_media_message(webhook, user_profile)
551
+ elif message_type == "location":
552
+ return await handlers.handle_location_message(webhook, user_profile)
553
+ elif message_type in ["contact", "contacts"]:
554
+ return await handlers.handle_contact_message(webhook, user_profile)
555
+ elif message_type == "interactive":
556
+ return await handlers.handle_interactive_message(webhook, user_profile)
557
+ else:
558
+ logger.warning(f"Unsupported message type: {message_type}")
559
+ return {"success": False, "error": f"Unsupported message type: {message_type}"}