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,492 @@
1
+ """
2
+ State handlers for interactive features in the Wappa Full Example application.
3
+
4
+ This module provides handlers for managing interactive states like button and list
5
+ selections, including state validation, response processing, and cleanup.
6
+ """
7
+
8
+ import time
9
+ from typing import Dict
10
+
11
+ from wappa.webhooks import IncomingMessageWebhook
12
+
13
+ from ..models.state_models import ButtonState, ListState, StateType
14
+ from ..models.user_models import UserProfile
15
+ from ..utils.cache_utils import CacheHelper
16
+ from ..utils.media_handler import send_local_media_file
17
+ from ..utils.metadata_extractor import MetadataExtractor
18
+
19
+
20
+ class StateHandlers:
21
+ """Collection of handlers for interactive state management."""
22
+
23
+ def __init__(self, messenger, cache_factory, logger):
24
+ """
25
+ Initialize state handlers.
26
+
27
+ Args:
28
+ messenger: IMessenger instance for sending messages
29
+ cache_factory: Cache factory for data persistence
30
+ logger: Logger instance
31
+ """
32
+ self.messenger = messenger
33
+ self.cache_helper = CacheHelper(cache_factory)
34
+ self.logger = logger
35
+
36
+ async def handle_button_state_response(self, webhook: IncomingMessageWebhook,
37
+ user_profile: UserProfile,
38
+ button_state: ButtonState) -> Dict[str, any]:
39
+ """
40
+ Handle response when user is in button state.
41
+
42
+ Args:
43
+ webhook: IncomingMessageWebhook with user input
44
+ user_profile: User profile for tracking
45
+ button_state: Active button state
46
+
47
+ Returns:
48
+ Result dictionary with operation status
49
+ """
50
+ try:
51
+ start_time = time.time()
52
+
53
+ user_id = webhook.user.user_id
54
+ message_id = webhook.message.message_id
55
+ message_type = webhook.get_message_type_name()
56
+
57
+ self.logger.info(f"🔘 Processing button state response from {user_id}")
58
+
59
+ # Check if this is an interactive button selection
60
+ if message_type == "interactive":
61
+ selection_id = webhook.get_interactive_selection()
62
+
63
+ if button_state.is_valid_selection(selection_id):
64
+ # Valid button selection - process it
65
+ return await self._process_button_selection(
66
+ webhook, user_profile, button_state, selection_id, start_time
67
+ )
68
+ else:
69
+ # Invalid selection
70
+ await self._send_invalid_button_selection_message(user_id, message_id, selection_id)
71
+ button_state.increment_attempts()
72
+ await self.cache_helper.save_user_state(button_state)
73
+
74
+ return {
75
+ "success": False,
76
+ "error": "Invalid button selection",
77
+ "selection_id": selection_id,
78
+ "attempts": button_state.attempts
79
+ }
80
+ else:
81
+ # Non-interactive message while in button state - send reminder
82
+ await self._send_button_state_reminder(user_id, message_id, button_state)
83
+ button_state.increment_attempts()
84
+ await self.cache_helper.save_user_state(button_state)
85
+
86
+ return {
87
+ "success": False,
88
+ "error": "Expected button selection",
89
+ "message_type": message_type,
90
+ "reminder_sent": True,
91
+ "attempts": button_state.attempts
92
+ }
93
+
94
+ except Exception as e:
95
+ self.logger.error(f"❌ Error handling button state response: {e}", exc_info=True)
96
+ return {"success": False, "error": str(e)}
97
+
98
+ async def handle_list_state_response(self, webhook: IncomingMessageWebhook,
99
+ user_profile: UserProfile,
100
+ list_state: ListState) -> Dict[str, any]:
101
+ """
102
+ Handle response when user is in list state.
103
+
104
+ Args:
105
+ webhook: IncomingMessageWebhook with user input
106
+ user_profile: User profile for tracking
107
+ list_state: Active list state
108
+
109
+ Returns:
110
+ Result dictionary with operation status
111
+ """
112
+ try:
113
+ start_time = time.time()
114
+
115
+ user_id = webhook.user.user_id
116
+ message_id = webhook.message.message_id
117
+ message_type = webhook.get_message_type_name()
118
+
119
+ self.logger.info(f"📋 Processing list state response from {user_id}")
120
+
121
+ # Check if this is an interactive list selection
122
+ if message_type == "interactive":
123
+ selection_id = webhook.get_interactive_selection()
124
+
125
+ if list_state.is_valid_selection(selection_id):
126
+ # Valid list selection - process it
127
+ return await self._process_list_selection(
128
+ webhook, user_profile, list_state, selection_id, start_time
129
+ )
130
+ else:
131
+ # Invalid selection
132
+ await self._send_invalid_list_selection_message(user_id, message_id, selection_id)
133
+ list_state.increment_attempts()
134
+ await self.cache_helper.save_user_state(list_state)
135
+
136
+ return {
137
+ "success": False,
138
+ "error": "Invalid list selection",
139
+ "selection_id": selection_id,
140
+ "attempts": list_state.attempts
141
+ }
142
+ else:
143
+ # Non-interactive message while in list state - send reminder
144
+ await self._send_list_state_reminder(user_id, message_id, list_state)
145
+ list_state.increment_attempts()
146
+ await self.cache_helper.save_user_state(list_state)
147
+
148
+ return {
149
+ "success": False,
150
+ "error": "Expected list selection",
151
+ "message_type": message_type,
152
+ "reminder_sent": True,
153
+ "attempts": list_state.attempts
154
+ }
155
+
156
+ except Exception as e:
157
+ self.logger.error(f"❌ Error handling list state response: {e}", exc_info=True)
158
+ return {"success": False, "error": str(e)}
159
+
160
+ async def _process_button_selection(self, webhook: IncomingMessageWebhook,
161
+ user_profile: UserProfile, button_state: ButtonState,
162
+ selection_id: str, start_time: float) -> Dict[str, any]:
163
+ """Process valid button selection."""
164
+ user_id = webhook.user.user_id
165
+ message_id = webhook.message.message_id
166
+
167
+ # Handle the selection in the state
168
+ button_state.handle_selection(selection_id)
169
+ selected_button = button_state.get_selected_button()
170
+
171
+ # Remove state from cache
172
+ await self.cache_helper.remove_user_state(user_id, StateType.BUTTON)
173
+
174
+ # Extract and format metadata
175
+ metadata = MetadataExtractor.extract_metadata(webhook, start_time)
176
+ metadata_text = MetadataExtractor.format_metadata_for_user(metadata)
177
+
178
+ # Send metadata response
179
+ await self.messenger.send_text(
180
+ recipient=user_id,
181
+ text=metadata_text,
182
+ reply_to_message_id=message_id
183
+ )
184
+
185
+ # Send corresponding media file based on selection
186
+ media_sent = False
187
+ media_file = None
188
+
189
+ if selection_id == "kitty":
190
+ media_file = "kitty.png"
191
+ elif selection_id == "puppy":
192
+ media_file = "puppy.png"
193
+
194
+ if media_file:
195
+ media_result = await send_local_media_file(
196
+ messenger=self.messenger,
197
+ recipient=user_id,
198
+ filename=media_file,
199
+ media_subdir="buttons",
200
+ caption=f"Here's your {selection_id}! 🎉"
201
+ )
202
+ media_sent = media_result["success"]
203
+
204
+ if not media_sent:
205
+ # Send fallback text if media fails
206
+ fallback_text = f"🎉 You selected: *{selected_button['title']}*\n\n(Media file not found: {media_file})"
207
+ await self.messenger.send_text(
208
+ recipient=user_id,
209
+ text=fallback_text
210
+ )
211
+
212
+ # Send completion message
213
+ completion_text = (
214
+ f"✅ *Button Selection Complete!*\n\n"
215
+ f"🎯 *Your choice*: {selected_button['title']}\n"
216
+ f"🆔 *Selection ID*: `{selection_id}`\n"
217
+ f"⏱️ *Response time*: {int((time.time() - start_time) * 1000)}ms\n"
218
+ f"🗑️ *State cleaned up*: Button state removed\n\n"
219
+ f"💡 *What happened*: You successfully clicked a button, received metadata, "
220
+ f"and got your chosen animal {'image' if media_sent else '(image failed to load)'}!"
221
+ )
222
+
223
+ await self.messenger.send_text(
224
+ recipient=user_id,
225
+ text=completion_text
226
+ )
227
+
228
+ # Update user activity
229
+ await self.cache_helper.update_user_activity(user_id, "interactive", interaction_type="button")
230
+
231
+ processing_time = int((time.time() - start_time) * 1000)
232
+ self.logger.info(f"✅ Button selection processed in {processing_time}ms")
233
+
234
+ return {
235
+ "success": True,
236
+ "selection_type": "button",
237
+ "selection_id": selection_id,
238
+ "selected_button": selected_button,
239
+ "metadata_sent": True,
240
+ "media_sent": media_sent,
241
+ "media_file": media_file,
242
+ "state_cleaned": True,
243
+ "processing_time_ms": processing_time
244
+ }
245
+
246
+ async def _process_list_selection(self, webhook: IncomingMessageWebhook,
247
+ user_profile: UserProfile, list_state: ListState,
248
+ selection_id: str, start_time: float) -> Dict[str, any]:
249
+ """Process valid list selection."""
250
+ user_id = webhook.user.user_id
251
+ message_id = webhook.message.message_id
252
+
253
+ # Handle the selection in the state
254
+ list_state.handle_selection(selection_id)
255
+ selected_item = list_state.get_selected_item()
256
+
257
+ # Remove state from cache
258
+ await self.cache_helper.remove_user_state(user_id, StateType.LIST)
259
+
260
+ # Extract and format metadata
261
+ metadata = MetadataExtractor.extract_metadata(webhook, start_time)
262
+ metadata_text = MetadataExtractor.format_metadata_for_user(metadata)
263
+
264
+ # Send metadata response
265
+ await self.messenger.send_text(
266
+ recipient=user_id,
267
+ text=metadata_text,
268
+ reply_to_message_id=message_id
269
+ )
270
+
271
+ # Send corresponding media file based on selection
272
+ media_sent = False
273
+ media_file = None
274
+ media_type = None
275
+
276
+ # Map selection to media file
277
+ if selection_id == "image_file":
278
+ media_file = "image.png"
279
+ media_type = "image"
280
+ elif selection_id == "video_file":
281
+ media_file = "video.mp4"
282
+ media_type = "video"
283
+ elif selection_id == "audio_file":
284
+ media_file = "audio.mp3"
285
+ media_type = "audio"
286
+ elif selection_id == "document_file":
287
+ media_file = "document.pdf"
288
+ media_type = "document"
289
+
290
+ if media_file and media_type:
291
+ media_result = await send_local_media_file(
292
+ messenger=self.messenger,
293
+ recipient=user_id,
294
+ filename=media_file,
295
+ media_subdir="list",
296
+ caption=f"Here's your {media_type} file! 🎉"
297
+ )
298
+ media_sent = media_result["success"]
299
+
300
+ if not media_sent:
301
+ # Send fallback text if media fails
302
+ fallback_text = f"🎉 You selected: *{selected_item['title']}*\n\n(Media file not found: {media_file})"
303
+ await self.messenger.send_text(
304
+ recipient=user_id,
305
+ text=fallback_text
306
+ )
307
+
308
+ # Send completion message
309
+ completion_text = (
310
+ f"✅ *List Selection Complete!*\n\n"
311
+ f"🎯 *Your choice*: {selected_item['title']}\n"
312
+ f"📝 *Description*: {selected_item.get('description', 'N/A')}\n"
313
+ f"🆔 *Selection ID*: `{selection_id}`\n"
314
+ f"📁 *Media type*: {media_type}\n"
315
+ f"⏱️ *Response time*: {int((time.time() - start_time) * 1000)}ms\n"
316
+ f"🗑️ *State cleaned up*: List state removed\n\n"
317
+ f"💡 *What happened*: You successfully selected from a list, received metadata, "
318
+ f"and got your chosen {media_type} {'file' if media_sent else '(file failed to load)'}!"
319
+ )
320
+
321
+ await self.messenger.send_text(
322
+ recipient=user_id,
323
+ text=completion_text
324
+ )
325
+
326
+ # Update user activity
327
+ await self.cache_helper.update_user_activity(user_id, "interactive", interaction_type="list")
328
+
329
+ processing_time = int((time.time() - start_time) * 1000)
330
+ self.logger.info(f"✅ List selection processed in {processing_time}ms")
331
+
332
+ return {
333
+ "success": True,
334
+ "selection_type": "list",
335
+ "selection_id": selection_id,
336
+ "selected_item": selected_item,
337
+ "metadata_sent": True,
338
+ "media_sent": media_sent,
339
+ "media_file": media_file,
340
+ "media_type": media_type,
341
+ "state_cleaned": True,
342
+ "processing_time_ms": processing_time
343
+ }
344
+
345
+ async def _send_button_state_reminder(self, user_id: str, message_id: str,
346
+ button_state: ButtonState) -> None:
347
+ """Send reminder message when user is in button state."""
348
+ time_remaining = button_state.time_remaining_minutes()
349
+
350
+ reminder_text = (
351
+ f"🔘 *Hey Wappa! We love your enthusiasm, but please press a button!*\n\n"
352
+ f"⚠️ *You're currently in Button Demo mode*\n"
353
+ f"📋 *Expected action*: Click one of the buttons above\n"
354
+ f"⏰ *Time remaining*: {time_remaining} minutes\n"
355
+ f"🔢 *Attempt*: {button_state.attempts + 1}/{button_state.max_attempts}\n\n"
356
+ f"💡 *Tip*: Look for the message with 🐱 Kitty and 🐶 Puppy buttons above!"
357
+ )
358
+
359
+ await self.messenger.send_text(
360
+ recipient=user_id,
361
+ text=reminder_text,
362
+ reply_to_message_id=message_id
363
+ )
364
+
365
+ async def _send_list_state_reminder(self, user_id: str, message_id: str,
366
+ list_state: ListState) -> None:
367
+ """Send reminder message when user is in list state."""
368
+ time_remaining = list_state.time_remaining_minutes()
369
+
370
+ reminder_text = (
371
+ f"📋 *Hey Wappa! We love your enthusiasm, but please make a list selection!*\n\n"
372
+ f"⚠️ *You're currently in List Demo mode*\n"
373
+ f"📋 *Expected action*: Tap 'Choose Media' and select an option\n"
374
+ f"⏰ *Time remaining*: {time_remaining} minutes\n"
375
+ f"🔢 *Attempt*: {list_state.attempts + 1}/{list_state.max_attempts}\n\n"
376
+ f"💡 *Tip*: Look for the message with the 'Choose Media' button above!"
377
+ )
378
+
379
+ await self.messenger.send_text(
380
+ recipient=user_id,
381
+ text=reminder_text,
382
+ reply_to_message_id=message_id
383
+ )
384
+
385
+ async def _send_invalid_button_selection_message(self, user_id: str, message_id: str,
386
+ selection_id: str) -> None:
387
+ """Send message for invalid button selection."""
388
+ error_text = (
389
+ f"❌ *Invalid Button Selection*\n\n"
390
+ f"🆔 *You selected*: `{selection_id}`\n"
391
+ f"✅ *Valid options*: `kitty`, `puppy`\n\n"
392
+ f"💡 *Please try again*: Click one of the valid buttons above!"
393
+ )
394
+
395
+ await self.messenger.send_text(
396
+ recipient=user_id,
397
+ text=error_text,
398
+ reply_to_message_id=message_id
399
+ )
400
+
401
+ async def _send_invalid_list_selection_message(self, user_id: str, message_id: str,
402
+ selection_id: str) -> None:
403
+ """Send message for invalid list selection."""
404
+ error_text = (
405
+ f"❌ *Invalid List Selection*\n\n"
406
+ f"🆔 *You selected*: `{selection_id}`\n"
407
+ f"✅ *Valid options*: `image_file`, `video_file`, `audio_file`, `document_file`\n\n"
408
+ f"💡 *Please try again*: Use the list above to make a valid selection!"
409
+ )
410
+
411
+ await self.messenger.send_text(
412
+ recipient=user_id,
413
+ text=error_text,
414
+ reply_to_message_id=message_id
415
+ )
416
+
417
+
418
+ # Convenience functions for direct use
419
+ async def handle_user_in_state(webhook: IncomingMessageWebhook, user_profile: UserProfile,
420
+ messenger, cache_factory, logger) -> Dict[str, any]:
421
+ """
422
+ Handle user response when they are in an active state (convenience function).
423
+
424
+ Args:
425
+ webhook: IncomingMessageWebhook with user response
426
+ user_profile: User profile for tracking
427
+ messenger: IMessenger instance
428
+ cache_factory: Cache factory
429
+ logger: Logger instance
430
+
431
+ Returns:
432
+ Result dictionary or None if no active state
433
+ """
434
+ handlers = StateHandlers(messenger, cache_factory, logger)
435
+ cache_helper = CacheHelper(cache_factory)
436
+ user_id = webhook.user.user_id
437
+
438
+ # Check for active button state
439
+ button_state = await cache_helper.get_user_state(user_id, StateType.BUTTON)
440
+ if button_state and button_state.is_active():
441
+ # Ensure we have the correct type
442
+ if isinstance(button_state, ButtonState):
443
+ return await handlers.handle_button_state_response(webhook, user_profile, button_state)
444
+ else:
445
+ logger.warning(f"Button state returned wrong type: {type(button_state)}")
446
+
447
+ # Check for active list state
448
+ list_state = await cache_helper.get_user_state(user_id, StateType.LIST)
449
+ if list_state and list_state.is_active():
450
+ # Ensure we have the correct type
451
+ if isinstance(list_state, ListState):
452
+ return await handlers.handle_list_state_response(webhook, user_profile, list_state)
453
+ else:
454
+ logger.warning(f"List state returned wrong type: {type(list_state)}")
455
+
456
+ # No active state found
457
+ return None
458
+
459
+
460
+ async def cleanup_expired_user_states(cache_factory, logger, user_id: str = None) -> int:
461
+ """
462
+ Cleanup expired states for a specific user or all users (convenience function).
463
+
464
+ Args:
465
+ cache_factory: Cache factory
466
+ logger: Logger instance
467
+ user_id: Optional specific user ID to clean up
468
+
469
+ Returns:
470
+ Number of states cleaned up
471
+ """
472
+ cache_helper = CacheHelper(cache_factory)
473
+
474
+ try:
475
+ # This is a simplified cleanup - in a real implementation you would
476
+ # scan Redis keys and clean up expired states
477
+ cleanup_count = 0
478
+
479
+ if user_id:
480
+ # Clean up specific user's states
481
+ for state_type in [StateType.BUTTON, StateType.LIST]:
482
+ state = await cache_helper.get_user_state(user_id, state_type)
483
+ if state and state.is_expired():
484
+ await cache_helper.remove_user_state(user_id, state_type)
485
+ cleanup_count += 1
486
+ logger.info(f"🧹 Cleaned up expired {state_type.value} state for user {user_id}")
487
+
488
+ return cleanup_count
489
+
490
+ except Exception as e:
491
+ logger.error(f"❌ Error during state cleanup: {e}")
492
+ return 0