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