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